In PHP, what do PDO's ERRMODE_SILENT, ERRMODE_WARNING and ERRMODE_EXCEPTION do, and why did PHP 8.0's default change matter?
answer
- false plus errorInfo() vs E_WARNING vs throw
- silent was the default before 8.0
- getCode() holds the SQLSTATE string
- errorInfo: SQLSTATE, driver code, message
- class 23: integrity constraint
basics
~20 sSILENT 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
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
Know the three error modes and that PHP 8 throws PDOException by default when a query fails.
Explain what silent mode caused before 8.0, what getCode() and errorInfo hold, and why the constructor always throws.
Handle expected constraint violations by SQLSTATE class, rethrow the rest, and avoid feeding the string code into an int parameter.
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