Since PHP 8.1, what does mysqli do by default when a query fails, and what breaks in legacy code that checks return values?
answer
- default flipped from silent
- MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT
- mysqli_sql_exception extends RuntimeException
- if ($res === false) never runs
- mysqli_report() is per request
basics
~10 sSince PHP 8.1 the default report mode is MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT, so failed connects and queries throw mysqli_sql_exception. Legacy checks like if ($res === false) or if (!$link) never run.
solid answer
~40 sBefore PHP 8.1 mysqli was silent: a failing `mysqli_query()` returned `false` and you read `mysqli_error()`. Since 8.1 the default is `MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT`, so connection, query, prepare and execute failures throw `mysqli_sql_exception`, a `RuntimeException`. In a legacy app the `if (!$link)` and `if ($res === false)` branches become dead code, the fallback logic never runs, and the request ends in an uncaught exception unless something catches it. I migrate by catching `mysqli_sql_exception` where I can recover, checking `getCode()` or `getSqlState()`, and letting the rest reach the global handler. `mysqli_report(MYSQLI_REPORT_OFF)` restores the old behaviour, but it applies to every connection in the request.
code
php · 16 lines<?php
declare(strict_types=1);
$db = new mysqli('db', 'app', $password, 'timetable'); // throws on failure
try {
$db->execute_query(
'INSERT INTO bookings (room_id, lesson_id) VALUES (?, ?)',
[$roomId, $lessonId]
);
} catch (mysqli_sql_exception $e) {
if ($e->getCode() !== 1062) { // not a duplicate-key error
throw $e;
}
$message = 'That room is already booked for this lesson.';
}go deeper
Recall that since PHP 8.1 a failing mysqli query throws mysqli_sql_exception instead of returning false.
Explain the report-mode flags, what ERROR and STRICT each contribute, and why old if ($res === false) checks go dead.
Plan the upgrade of a legacy mysqli app: find the dead error branches, add targeted catches, and avoid a blanket MYSQLI_REPORT_OFF.
Set the policy for mixed codebases that use both PDO and mysqli: one error-handling contract and one global handler for both exception types.
## The default changed in PHP 8.1 The mysqli extension has a **report mode** that decides what happens when a call fails. It is set with `mysqli_report(int $flags)` and combines these flags: | Flag | Effect | |---|---| | `MYSQLI_REPORT_OFF` | report nothing; failing calls just return `false` | | `MYSQLI_REPORT_ERROR` | report errors from mysqli calls | | `MYSQLI_REPORT_STRICT` | report them as `mysqli_sql_exception` instead of warnings | | `MYSQLI_REPORT_INDEX` | also report queries that used no index or a bad one | | `MYSQLI_REPORT_ALL` | all of the above | Before PHP 8.1 the default was `MYSQLI_REPORT_OFF`. Since **PHP 8.1** it is `MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT`: a failed connection, query, `prepare()` or `execute()` throws `mysqli_sql_exception`, which extends `RuntimeException`. The migration guide describes it as the default error handling mode changing from "silent" to "exceptions". Two details of the mechanism matter in practice: - The mode is **per request**, not per connection. The extension resets it to `ERROR | STRICT` at the start of every request, and a `mysqli_report()` call affects every mysqli connection for the rest of that request. - `MYSQLI_REPORT_ERROR` without `STRICT` raises an `E_WARNING` instead of throwing. ## What breaks in legacy code A legacy school-timetable app written for PHP 7 checks return values everywhere: ```php $link = mysqli_connect('db', 'app', $pass, 'timetable'); if (!$link) { die('Connection failed: ' . mysqli_connect_error()); } $res = mysqli_query($link, $sql); if ($res === false) { log_error(mysqli_error($link)); $res = fallback_timetable(); } ``` On PHP 8.1 and later: 1. `mysqli_connect()` **throws** on failure, so `if (!$link)` never runs. 2. `mysqli_query()` **throws** on a bad query, so the fallback branch is dead code and the request ends with an uncaught exception. 3. With `display_errors` on, the uncaught exception's message and trace can reach the page, including parts of the failing SQL. 4. Tests written against the old behaviour, asserting that a function returns `false`, start failing. Nothing about the code looks wrong in review; it simply no longer runs its error branches. ## Migrating properly - **Catch where you can recover.** A duplicate booking of a room can be caught and turned into a friendly message: `catch (mysqli_sql_exception $e)`, then inspect `$e->getCode()` (the MySQL error number) or `$e->getSqlState()`. - **Let everything else propagate** to the application's global error handler, which logs it and returns an error page. - **Delete dead `if ($res === false)` branches** once exceptions are confirmed, so readers are not misled. - **Use `mysqli_report(MYSQLI_REPORT_OFF)` only as a stop-gap** during an upgrade, and only in the legacy entry points that still depend on it, because it silences failures for every connection in the request. ## Finding the dead branches Before or right after an upgrade, search the codebase for the patterns that stopped working: 1. `=== false` or `!$res` checks right after `mysqli_query()`, `mysqli_prepare()` or `mysqli_stmt_execute()`. 2. `mysqli_error(` and `mysqli_connect_error(` calls used to build error messages. 3. `or die(` after a connect or query call — a common legacy idiom that now never fires. 4. `@` in front of mysqli calls, which hid warnings under the old mode and hides nothing useful now. Each hit is either dead code to delete or a recovery path to turn into a `catch`. ## The `MYSQLI_REPORT_ALL` trap Setting `MYSQLI_REPORT_ALL` looks like "maximum safety", but it includes `MYSQLI_REPORT_INDEX`. Combined with `STRICT`, every query that the server reports as using no index — even a harmless lookup on a tiny `rooms` table — throws `mysqli_sql_exception` with a message like "No index used in query/prepared statement". That setting belongs in a diagnostic session, not in production. ## Comparing with PDO PDO made the same move one release earlier: since PHP 8.0 its default error mode is exceptions. The two extensions throw different classes, `PDOException` and `mysqli_sql_exception`, so a codebase using both needs both in any `catch` that handles database errors. ## Summary In PHP 8.5, as since 8.1, a failing mysqli call throws `mysqli_sql_exception`. Code that relies on `false` returns and `mysqli_error()` checks keeps compiling but stops executing its error branches, so upgrades must replace those checks with targeted `catch` blocks.
- What does MYSQLI_REPORT_ERROR without MYSQLI_REPORT_STRICT do?It still reports failures, but as `E_WARNING` messages instead of exceptions; the call then returns `false` as before. That mode suits code that checks return values and wants a log line, but warnings are easy to lose, so current code keeps `STRICT` on.
- Why is mysqli_report(MYSQLI_REPORT_OFF) in a shared bootstrap file risky?The report mode is a per-request global, not a connection setting. Calling it in a bootstrap turns off exceptions for every mysqli connection in every request that loads the file, including new code that relies on them, so failures silently return `false` again.
saying these in an interview costs you the question
- Believing mysqli still returns false silently by default in PHP 8.
- Thinking the report mode is set per connection object.
- Treating MYSQLI_REPORT_ALL as the safest production setting.
- Catching Exception around mysqli calls and discarding every failure.
- Assuming a failed mysqli_connect() still returns false for an if check.