phacility/phabricator · error · AphrontDuplicateKeyQueryException
1062
1062
Error message
#%d: %s
What it means
MySQL returned error 1062 (duplicate entry for a UNIQUE or PRIMARY key) while executing a query, and the connection layer maps it to a typed AphrontDuplicateKeyQueryException so callers can distinguish constraint collisions from other failures. The key name is deliberately not parsed out of the message because older MySQL servers only report a key index like 'key 2'. This exception is a plain AphrontQueryException, not AphrontRecoverableQueryException, so blindly retrying the identical INSERT will fail again.
Source
Thrown at src/infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php:346
case 2013: // Connection Dropped
throw new AphrontConnectionLostQueryException($message);
case 2006: // Gone Away
$more = pht(
'This error may occur if your configured MySQL "wait_timeout" or '.
'"max_allowed_packet" values are too small. This may also indicate '.
'that something used the MySQL "KILL <process>" command to kill '.
'the connection running the query.');
throw new AphrontConnectionLostQueryException("{$message}\n\n{$more}");
case 1213: // Deadlock
throw new AphrontDeadlockQueryException($message);
case 1205: // Lock wait timeout exceeded
throw new AphrontLockTimeoutQueryException($message);
case 1062: // Duplicate Key
// NOTE: In some versions of MySQL we get a key name back here, but
// older versions just give us a key index ("key 2") so it's not
// portable to parse the key out of the error and attach it to the
// exception.
throw new AphrontDuplicateKeyQueryException($message);
case 1044: // Access denied to database
case 1142: // Access denied to table
case 1143: // Access denied to column
case 1227: // Access denied (e.g., no SUPER for SHOW SLAVE STATUS).
// See T13622. Try to help users figure out that this is a GRANT
// problem.
$more = pht(
'This error usually indicates that you need to "GRANT" the '.
'MySQL user additional permissions. See "GRANT" in the MySQL '.
'manual for help.');
throw new AphrontAccessDeniedQueryException("{$message}\n\n{$more}");
case 1045: // Access denied (auth)
throw new AphrontInvalidCredentialsQueryException($message);
case 1146: // No such table
case 1049: // No such databaseView on GitHub (pinned to 5720a38cfe)
Solutions
- Catch AphrontDuplicateKeyQueryException and treat it as 'already exists': load the existing row with loadOneWhere() and continue instead of failing
- Make the writer idempotent: do the check-then-insert inside a transaction, or rely on the catch as the arbiter rather than a pre-check
- Deduplicate existing data before applying a patch that adds a unique index
- Only if the upsert semantics are intended, use INSERT ... ON DUPLICATE KEY UPDATE / REPLACE deliberately, not as a band-aid
Example fix
// before: naive create, dies on concurrent insert
$obj = MyDAO::initializeNewObject($name);
$obj->save();
// after: adopt the row the winner created
try {
$obj->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
$obj = id(new MyDAO())->loadOneWhere('objectName = %s', $name);
if (!$obj) {
throw $ex;
}
} Defensive patterns
Strategy: try-catch
Validate before calling
// Advisory pre-check only — the UNIQUE index is the real arbiter under concurrency:
$existing = id(new MyDAO())->loadOneWhere(
'%C = %s',
$unique_column,
$value);
if ($existing) {
return $existing;
} Try / catch
try {
$object->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// Lost the race: adopt the winner's row instead of failing.
$object = id(new MyDAO())->loadOneWhere(
'%C = %s',
$unique_column,
$value);
if (!$object) {
throw $ex; // row vanished again — surface it
}
} Prevention
- Model create-or-get as INSERT + catch AphrontDuplicateKeyQueryException, never as bare check-then-insert outside a transaction
- Keep UNIQUE indexes on every column whose uniqueness the code assumes, so races cannot create duplicates
- Deduplicate data before deploying a patch that adds a unique index
- Remember this exception is not AphrontRecoverableQueryException — do not blind-retry the same INSERT
When it happens
Trigger: Calling $dao->save()/insert() or a raw INSERT/UPDATE that collides with an existing unique value; two workers concurrently inserting the same PHID/username/token (check-then-insert without a transaction); replaying a data script that assumes a row is absent; a newly added unique index colliding with pre-existing duplicate rows.
Common situations: Race conditions where parallel daemons or web requests create the same record; migrations that add a UNIQUE constraint over dirty data; re-running import scripts that are not idempotent; generating identifiers externally and inserting them without deduplication.
Related errors
- Unable to load MySQL blob file '%s'!
- 1146, 1049, 1054
- This contact number is already in use.
- This public key is already associated with another user or d
- Two authentication providers use the same provider key ('%s'
AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21).
Data as JSON: /api/errors/9d1a575d5405184a.
Report an issue: GitHub.