Isolation level issue
Answer originally left by Dan Guzman in comments:

The method I usually use is an explicit transaction plus a UPDLOCK, HOLDLOCK locking hint on the SELECT query. That will help avoid deadlocks and avoid the race condition, without resorting to serializable isolation for the entire transaction.

Also SERIALIZABLE won't really help as the initial select will get shared (S) range locks.  So both sessions will get the S lock, and then, instead of a PK violation, you'll get a deadlock.

With the UPDLOCK in addition to HOLDLOCK, only one session can select the same key regardless of whether it exists (holding update key lock) or not (holding update key range lock). This will serialize access to the specified key in this code path.

This worked for the OP, who implemented it as follows:

@Id = SELECT *
  FROM ANIMAL WITH(UPDLOCK, HOLDLOCK)
  WHERE Owner = x AND Name = y
IF(@id &lt;&gt; NULL)
  UPDATE ANIMAL
ELSE
  INSERT ANIMAL

--DO OTHER STUFF