When a query fails, PDO (with ERRMODE_EXCEPTION) throws a PDOException. General error handling is in Database Errors. This lesson shows how to read MySQL’s own error number, so your code can tell a duplicate email from a deadlock.
Where the details are
<?php
$pdo = require __DIR__ . '/db.php';
try {
$pdo->query('SELECT missing_column FROM products');
} catch (PDOException $e) {
echo 'getCode(): ', $e->getCode(), "\n"; // the SQLSTATE
echo 'errorInfo[0]: ', $e->errorInfo[0], "\n"; // SQLSTATE (5 characters)
echo 'errorInfo[1]: ', $e->errorInfo[1], "\n"; // the MySQL error number
echo 'errorInfo[2]: ', $e->errorInfo[2], "\n"; // the message text
}
getCode(): 42S22
errorInfo[0]: 42S22
errorInfo[1]: 1054
errorInfo[2]: Unknown column 'missing_column' in 'field list'
Note that getCode() returns the SQLSTATE (a string such as 42S22), while errorInfo[1] is the MySQL error number (1054), and that is the one you can compare with an int. (For connection errors thrown by new PDO, getCode() returns the MySQL number such as 2002, as you saw earlier.)
The codes you will meet
| Code | SQLSTATE | Meaning | Typical cause and response |
|---|---|---|---|
| 1062 | 23000 | Duplicate entry for a unique key | Email already registered. Tell the user |
| 1451 | 23000 | Cannot delete or update a parent row: a foreign key constraint fails | Deleting a customer who has orders. Explain, or archive instead |
| 1452 | 23000 | Cannot add or update a child row: a foreign key constraint fails | The parent does not exist. A bug or a stale form |
| 1048 | 23000 | Column cannot be null | Missing required value. Validate earlier |
| 1364 | HY000 | Field doesn’t have a default value | The insert forgot a NOT NULL column |
| 1406 | 22001 | Data too long for column | Validate length before inserting |
| 1264 | 22003 | Out of range value | A number too large for the type |
| 1054 | 42S22 | Unknown column | A typo or a wrong deployment: a bug |
| 1146 | 42S02 | Table doesn’t exist | A missing migration: a bug |
| 1064 | 42000 | SQL syntax error | A bug (often dynamic SQL or LIMIT with a string) |
| 1213 | 40001 | Deadlock | Retry the transaction |
| 1205 | HY000 | Lock wait timeout | Retry, or shorten transactions |
| 1040 | HY000 | Too many connections | Server overloaded; back off |
| 2006 / 2013 | HY000 | Server has gone away / Lost connection | Reconnect, check wait_timeout and max_allowed_packet |
Group them: user mistakes (1062, 1048, 1406, 1451) deserve a friendly message. Bugs (1054, 1064, 1146) deserve a log entry, an alert and a general message. Transient errors (1213, 1205, 2006) deserve a retry.
A small mapping
<?php
$pdo = require __DIR__ . '/db.php';
function friendlyMessage(PDOException $e): ?string
{
return match ($e->errorInfo[1] ?? 0) {
1062 => 'That value is already in use.',
1451 => 'This record is still used by other records.',
1452 => 'The related record does not exist.',
1048 => 'A required value is missing.',
1406 => 'One of the values is too long.',
default => null, // not a user mistake: let the caller log it and show a general message
};
}
$attempts = [
['INSERT INTO customers (name, email) VALUES ("Dup", "maya@example.com")'],
['DELETE FROM customers WHERE id = 1'],
['INSERT INTO customers (name, email) VALUES (NULL, "n@example.com")'],
['INSERT INTO customers (name, email) VALUES (REPEAT("x", 80), "long@example.com")'],
['SELECT nothing FROM customers'],
];
foreach ($attempts as [$sql]) {
try {
$pdo->exec($sql);
} catch (PDOException $e) {
echo $e->errorInfo[1], ' => ', friendlyMessage($e) ?? '(general error, log it)', "\n";
}
}
1062 => That value is already in use.
1451 => This record is still used by other records.
1048 => A required value is missing.
1406 => One of the values is too long.
1054 => (general error, log it)
Log details, show little
<?php
$pdo = require __DIR__ . '/db.php';
try {
$pdo->query('SELECT * FROM missing_table');
} catch (PDOException $e) {
error_log(sprintf('[db] code=%s sqlstate=%s msg=%s', $e->errorInfo[1], $e->errorInfo[0], $e->errorInfo[2]));
http_response_code(500);
echo "Sorry, something went wrong. Please try again later.\n";
}
Sorry, something went wrong. Please try again later.
The visitor sees a general message. The log (error_log) holds the code, the SQLSTATE and the message, but not the parameter values, which may be personal data or secrets. Never print $e->getMessage() or the SQL on a public page: it reveals table names and helps attackers (SQL Injection).
Warnings are not exceptions
Some problems in MySQL are warnings, not errors (for example truncation in non-strict mode). PDO does not throw for warnings. Keep strict mode on, so that the same problems become real errors, and check with SHOW WARNINGS when debugging (Errors and Warnings).
Test the error paths
Write at least one test for each error you handle: insert a duplicate, delete a parent with children, and so on. Error branches that never run are where bugs hide.
Compare the number, not the message
errorInfo[1]) and the SQLSTATE are stable.