skip to content

In PHP, what do PDO's ERRMODE_SILENT, ERRMODE_WARNING and ERRMODE_EXCEPTION do, and why did PHP 8.0's default change matter?

level: middleimportance: must knowfreq 60%

answer

  1. false plus errorInfo() vs E_WARNING vs throw
  2. silent was the default before 8.0
  3. getCode() holds the SQLSTATE string
  4. errorInfo: SQLSTATE, driver code, message
  5. class 23: integrity constraint

basics

~20 s

SILENT makes failing calls return false and leaves details in errorInfo(); WARNING also emits E_WARNING; EXCEPTION throws PDOException. PHP 8.0 made EXCEPTION the default, so a failed query no longer slips by as an unchecked false.

solid answer

~40 s

`PDO::ATTR_ERRMODE` decides how a failing call reports itself. With **`ERRMODE_SILENT`** it returns `false` and you must check `errorCode()`/`errorInfo()` yourself; **`ERRMODE_WARNING`** does the same and also raises `E_WARNING`; **`ERRMODE_EXCEPTION`** throws `PDOException`. Before PHP 8.0 silent was the default, so code that forgot to check a `false` from `query()` or `execute()` lost writes silently or died later with a call on `bool`. Since 8.0 exceptions are the default. For a failed statement, `PDOException::getCode()` returns the **SQLSTATE as a string**, such as `'23000'`, and the public `errorInfo` array holds the SQLSTATE, the driver's own error code and its message. That is what you inspect to turn, say, a duplicate booking into a friendly "slot already taken".

code

php · 18 lines
php
<?php
declare(strict_types=1);

function bookSlot(PDO $pdo, int $doctorId, int $patientId, string $startsAt): bool
{
    $stmt = $pdo->prepare(
        'INSERT INTO appointments (doctor_id, patient_id, starts_at) VALUES (?, ?, ?)'
    );
    try {
        $stmt->execute([$doctorId, $patientId, $startsAt]);
        return true;
    } catch (PDOException $e) {
        if (str_starts_with((string) $e->getCode(), '23')) {
            return false;              // slot already taken: unique index hit
        }
        throw $e;                      // anything else is a real failure
    }
}

go deeper

for a junior

Know the three error modes and that PHP 8 throws PDOException by default when a query fails.

for a middle

Explain what silent mode caused before 8.0, what getCode() and errorInfo hold, and why the constructor always throws.

for a senior

Handle expected constraint violations by SQLSTATE class, rethrow the rest, and avoid feeding the string code into an int parameter.

for a principal

Define one error-translation layer that maps database failures to domain outcomes consistently, so each feature does not invent its own SQLSTATE checks.

## Three ways to report a failure `PDO::ATTR_ERRMODE` controls what happens when a PDO or `PDOStatement` method fails, for example a `query()` with a syntax error or an `execute()` that violates a unique index. | Mode | Return value on failure | Diagnostic | How code notices | |---|---|---|---| | `PDO::ERRMODE_SILENT` | `false` | none | check the return value, then `errorCode()` / `errorInfo()` | | `PDO::ERRMODE_WARNING` | `false` | `E_WARNING` with the SQLSTATE and message | the same checks, plus the log | | `PDO::ERRMODE_EXCEPTION` | nothing, the call throws | `PDOException` | `try`/`catch` or a global handler | The constructor is the exception to the table: a failed connection **always** throws `PDOException`, in every mode. ## Why the PHP 8.0 change mattered Before PHP 8.0, `ERRMODE_SILENT` was the default. That made two failure patterns common: 1. **Silent data loss.** `$stmt->execute([...])` for an `INSERT` returned `false`, nobody checked it, and the booking was never stored while the user saw a success page. 2. **Errors far from the cause.** `$pdo->query('SELEC ...')` returned `false`, and the next line died with `Call to a member function fetchAll() on bool`, pointing at the wrong line. PHP 8.0 switched the default to `ERRMODE_EXCEPTION`, so an unhandled database error now stops the request at the failing call with a message that names the SQLSTATE. Legacy checks like `if ($stmt === false)` become dead code under exceptions; they are harmless, but they suggest the author expected silent mode. ## What a PDOException carries - `PDOException` extends `RuntimeException`. - For a failed statement, **`getCode()` returns the SQLSTATE as a five-character string**, for example `'23000'` or `'42S02'`, not an int. Connection failures are raised differently and carry the driver's numeric error code instead. - The public **`errorInfo`** property is an array: `[0]` the SQLSTATE, `[1]` the driver-specific error code, `[2]` the driver's message. - `getMessage()` combines them, in the shape `SQLSTATE[23000]: Integrity constraint violation: ...` followed by the driver's code and text. Two consequences trip people up: - Passing `$e->getCode()` as the `int $code` of another exception fails. `Exception::__construct()` declares `int $code`, so a SQLSTATE string is rejected with `TypeError` under `strict_types`, and a non-numeric one such as `'HY000'` is rejected even without it. Pass `$e` as the previous exception instead. - Comparing `$e->getCode() === 23000` is always false because the value is a string. ## Handling one error on purpose In the clinic-booking app, a unique index on `(doctor_id, starts_at)` stops double booking. Two patients clicking the same slot produce one success and one `PDOException`. SQLSTATE values that start with `23` belong to the SQL standard's **integrity constraint violation** class: MySQL reports a duplicate key as `23000`, PostgreSQL a unique violation as `23505`. The handler checks that class, turns the error into a domain result, and rethrows everything else so real failures still surface. ## Auditing legacy code written for silent mode Code written for PHP 7 often contains patterns that only made sense when errors were silent: - `if (!$stmt->execute()) { ... }` branches that can no longer run, because a failure now throws before the `if` sees a value. - `$pdo->errorInfo()` checks after every call, which now only ever run after calls that succeeded. - `@` in front of PDO calls to hide warnings, which does not stop an exception. - Loops that continue after a failed insert, which now stop at the first failure unless the exception is caught per row. Each of these deserves a decision rather than a mechanical deletion: sometimes the old branch was the only place a failure was reported to the user. ## Logging the right fields When you log a `PDOException`, record the SQLSTATE (`getCode()`), the driver code (`errorInfo[1]`) and the message, plus the query name or location, but not the bound values, which can contain personal data such as patient names. ## Choosing the mode - Use `ERRMODE_EXCEPTION` for application code, and state it in the options array. - Treat `ERRMODE_SILENT` as a legacy mode; if a library needs it, the checks must be exhaustive. - `ERRMODE_WARNING` is mostly useful while debugging old code, since it only adds a log line to silent behaviour.

  • Where do you find the database's own error number, as opposed to the SQLSTATE?
    In `$e->errorInfo[1]` on the `PDOException`, or in `errorInfo()[1]` on the connection or statement in silent mode. Index 0 is the portable SQLSTATE, index 1 the driver's native code, index 2 the driver's message. Code that must run on several databases should prefer the SQLSTATE.
  • Why does new RuntimeException('Booking failed', $e->getCode(), $e) blow up?
    For a failed statement `getCode()` returns the SQLSTATE string, while `Exception::__construct()` declares `int $code`. Under `strict_types` any string is a `TypeError`, and a value like `'HY000'` fails even in weak mode. Pass `$e` as the previous exception and use your own int code, or none.

saying these in an interview costs you the question

  • PDO still returns false on errors by default in PHP 8
  • PDOException::getCode() returns the driver's integer error number for a failed query
  • ERRMODE_WARNING throws after logging the warning
  • The error mode also controls whether a failed connection throws
  • Catching PDOException and ignoring it is fine for INSERTs