PDO Error Handling

Su Subhalakshmi Chandran Updated 21 Oct 2026
5 min read ·Lesson 176 of 181
In this post

How PDO Reports Errors In simple terms: Database operations can fail: a duplicate email, a missing table, a lost connection, a deadlock. Good error handling means three things: the failure must never

How PDO Reports Errors

In simple terms: Database operations can fail: a duplicate email, a missing table, a lost connection, a deadlock. Good error handling means three things: the failure must never go unnoticed, the user must see a helpful message (not a technical one), and developers must get enough detail in the logs to fix the problem.

Technically: PDO has three error modes, set with PDO::ATTR_ERRMODE. Since PHP 8.0, the default is PDO::ERRMODE_EXCEPTION: every failure throws a PDOException. The exception carries a message, an SQLSTATE code (a standardized five-character code), and an errorInfo array with the database's own error code and message.

ModeBehavior
PDO::ERRMODE_EXCEPTIONThrows PDOException (default since PHP 8.0, recommended)
PDO::ERRMODE_WARNINGEmits a PHP warning and returns false
PDO::ERRMODE_SILENTReturns false; details only via errorCode() / errorInfo()

Anatomy of a PDOException

<?php
require __DIR__ . '/src/pdo.php';

try {
    pdo()->prepare('INSERT INTO customers (name, email) VALUES (?, ?)')
        ->execute(['Asha Again', 'asha@example.com']);
} catch (PDOException $e) {
    echo $e->getMessage() . "\n\n";
    var_dump($e->getCode());
    print_r($e->errorInfo);
}

Output

SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'asha@example.com' for key 'customers.uq_customers_email'

string(5) "23000"
Array
(
    [0] => 23000
    [1] => 1062
    [2] => Duplicate entry 'asha@example.com' for key 'customers.uq_customers_email'
)
PartContentUse
getMessage()Full message including SQLSTATE, driver code, and textLogging
getCode()The SQLSTATE as a string ("23000")Broad error category
errorInfo[0]SQLSTATESame as getCode()
errorInfo[1]The driver-specific error code (1062 for MySQL duplicates)Precise handling
errorInfo[2]The driver's error messageLogging

The SQLSTATE 23000 covers all integrity violations — duplicates, foreign key failures, NOT NULL violations. To react to a specific problem, check the MySQL code in errorInfo[1].

Common MySQL Error Codes

errorInfo[1]SQLSTATEMeaning
106223000Duplicate entry for a unique key
145123000Cannot delete/update a row referenced by a foreign key
145223000Foreign key points to a non-existent row
104823000Column cannot be NULL
3819HY000CHECK constraint violated
121340001Deadlock; the transaction was rolled back — retry
1205HY000Lock wait timeout exceeded
114642S02Table does not exist
106442000SQL syntax error
2006 / 2013HY000Connection to the server lost

Silent Mode: Why Exceptions Are Better

<?php
require __DIR__ . '/src/pdo.php';

$pdo = pdo();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);

$result = $pdo->query('SELECT * FROM missing_table');

var_dump($result);
print_r($pdo->errorInfo());

$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

Output

bool(false)
Array
(
    [0] => 42S02
    [1] => 1146
    [2] => Table 'gocourse_shop.missing_table' doesn't exist
)

In silent mode, query() merely returns false. Code that forgets to check — $pdo->query(...)->fetchAll() — fails later with an unrelated "Call to a member function fetchAll() on false" error. Silent and warning modes are relics of older PHP versions; keep the exception mode.


Translating Database Errors Into Application Errors

Code above the database layer should not need to know MySQL error numbers. A repository (a class responsible for loading and saving one kind of data) catches database exceptions and throws meaningful domain exceptions instead:

<?php
require __DIR__ . '/src/pdo.php';

final class EmailAlreadyRegisteredException extends RuntimeException {}

final class CustomerRepository
{
    public function __construct(private PDO $pdo) {}

    public function register(string $name, string $email, ?string $city): int
    {
        try {
            $this->pdo
                ->prepare('INSERT INTO customers (name, email, city) VALUES (:name, :email, :city)')
                ->execute(['name' => $name, 'email' => strtolower($email), 'city' => $city]);

            return (int) $this->pdo->lastInsertId();
        } catch (PDOException $e) {
            if (($e->errorInfo[1] ?? null) === 1062) {
                throw new EmailAlreadyRegisteredException("Email already registered: $email", 0, $e);
            }
            throw $e;
        }
    }
}

$customers = new CustomerRepository(pdo());

foreach ([['Asha K', 'ASHA@example.com'], ['Oviya', 'oviya@example.com']] as [$name, $email]) {
    try {
        $id = $customers->register($name, $email, 'Chennai');
        echo "Registered $name (ID $id)\n";
    } catch (EmailAlreadyRegisteredException) {
        echo "$email: This email address is already registered.\n";
    }
}

Output (the ID depends on your data)

ASHA@example.com: This email address is already registered.
Registered Oviya (ID 18)

(The failed duplicate inserts in this lesson still used up auto-increment values 16 and 17 — see MySQL Get Last ID.)

Why This Design Helps

  1. The calling code catches EmailAlreadyRegisteredException — a name that explains the problem — instead of inspecting PDOException codes.
  2. Only the duplicate case is translated; every other database error is re-thrown unchanged and reaches the global error handler.
  3. The original PDOException is kept as the previous exception, so logs still show the database details (see PHP Exceptions).
  4. The user sees a clear, actionable message.

Logging Database Errors

When an unexpected database error occurs, log enough context to investigate — but never secrets:

<?php
try {
    // ... database work ...
} catch (PDOException $e) {
    error_log(sprintf(
        'DB error %s (%s) in %s: %s',
        $e->errorInfo[1] ?? '?',
        $e->getCode(),
        'CheckoutService::placeOrder',
        $e->getMessage()
    ));

    throw $e;   // let the global handler show a generic error page
}
LogDo Not Log
Error codes and messagesPasswords, password hashes, tokens
Which operation failed (class, method, order ID)Full parameter lists that contain personal data
Request ID for correlationCredit card or bank details

The global exception handler (see Custom Error Handler) shows users a generic "Something went wrong" page with HTTP status 500 for anything not handled specifically.


Which Errors to Handle Where

ErrorHandleHow
Duplicate entries (1062)In the repository or serviceTranslate into a domain exception and a friendly form error
Foreign key violations (1451, 1452)In the serviceExplain why the action is not possible, or deactivate instead of deleting
Deadlocks (1213)Around the transactionRetry a limited number of times (see PDO Transactions)
Lost connection (2006, 2013)In long-running workersReconnect and retry the operation
Syntax errors, missing tablesNowhere locallyThese are bugs: let them reach the global handler and fix the code

Common Mistakes

Displaying $e->getMessage() to Users

Effect: Table names, queries, and data values are exposed.

Fix: Log details; show generic or translated messages.

Comparing getCode() With Integers

Effect: $e->getCode() === 1062 is never true — getCode() returns the SQLSTATE string.

Fix: Use $e->errorInfo[1] for MySQL error numbers.

Catching and Ignoring Exceptions

Effect: Failed writes go unnoticed and data is silently lost.

Fix: Handle specific cases; re-throw the rest.

Using Silent Mode

Effect: Failures surface as confusing errors elsewhere.

Fix: Keep PDO::ERRMODE_EXCEPTION.

Losing the Original Exception

Effect: Logs show only the translated message.

Fix: Pass the PDOException as the $previous argument.


Best Practices

  • Keep the exception error mode for all connections.
  • Use errorInfo[1] to detect specific database errors and SQLSTATE for broad categories.
  • Translate expected database errors into domain exceptions in the data-access layer.
  • Log technical details with context, never secrets, and show users friendly messages.
  • Let unexpected errors reach the global handler instead of hiding them.

Official reference: https://www.php.net/manual/en/pdo.error-handling.php

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else User-defined Functions Slicing Strings PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting Error Logging HTTP Headers and Redirects Output Buffering Sending Email PHP cURL Consuming REST APIs PHP Generators AJAX PHP AJAX Database AJAX XML AJAX Live Search AJAX Poll AJAX with JSON Security Introduction Password Hashing SQL Injection Prevention XSS Prevention Garbage Collection Project: Simple Blog PHP What is OOP PHP Classes/Objects The $this Keyword Access Array Items Update Array Items Add Array Items Remove Array Items Sorting Arrays Multidimensional Arrays Array Destructuring Array Spread Operator Search in Arrays Merge Arrays PHP Abstract Classes PHP Interfaces Polymorphism PHP Traits PHP Static Methods PHP Static Properties Late Static Binding Magic Methods Object Cloning MySQL Database MySQL Connect MySQL Create DB MySQL Create Table MySQL Insert Data MySQL Get Last ID MySQL Insert Multiple MySQL Prepared MySQL Select Data MySQL Where Debugging PHP Project: Image Upload Gallery $_SESSION $_ENV PHP RegEx RegEx Patterns Heredoc and Nowdoc RegEx Modifiers preg_match preg_replace preg_split PHP RegEx Functions Nullable Types Mixed and Never Types Nullsafe Operator PHP Attributes PHP Fibers Readonly Classes Property Hooks (PHP 8.4) Asymmetric Visibility (PHP 8.4) Reflection API SPL Data Structures Composer PHP Form Handling PDO Error Handling PHP SimpleXML Parser PHP SimpleXML - Get PHP XML Expat Parser PHP DOM Parser PHP XMLReader
Ask about this post
AI Ask about this post

Ask questions about PDO Error Handling and get answers drawn from it.

Signed-in readers only.