Turn PHP Warnings into Exceptions (and Catch Them Like an Adult)
fopen fails, PHP emits a warning, returns false... and your script keeps going, fwrite-ing to false, building a corrupted export nobody notices until month-end. Warnings are diagnostics pretending to be error handling. The fix is to stop pretending:
set_error_handler(function (int $severity, string $message, string $file, int $line) {
if (!(error_reporting() & $severity)) {
return false; // respect the @ operator and suppressed levels
}
throw new ErrorException($message, 0, $severity, $file, $line);
});
Every warning and notice now throws ErrorException - catchable, stack-traced, un-ignorable. The failed fopen becomes a try/catch at the call site or a 500 with a real trace, not a silent limp onward. That error_reporting() check preserves the semantics of @ and your configured levels, which keeps third-party code that (regrettably) relies on suppression working.
The scoped version: my actual recommendation
Converting warnings globally in a legacy codebase is turning every papered-over crack into a wall collapse at once - bold, but rarely survivable. Scope the strictness to code you're hardening:
function strictly(callable $fn): mixed
{
set_error_handler(fn ($s, $m, $f, $l) => throw new ErrorException($m, 0, $s, $f, $l));
try {
return $fn();
} finally {
restore_error_handler(); // ALWAYS restore - finally guarantees it
}
}
// File and network code is where warnings-as-failures matter most
$data = strictly(fn () => file_get_contents($importPath));
$rows = strictly(fn () => json_decode($data, true, 512, JSON_THROW_ON_ERROR));
New projects: global handler from day one. Existing ones: strictly() around I/O boundaries, expanding as tests permit.
What this can't catch - and what to do there
PHP 8 already converted many former warnings into real Error throwables (undefined functions, bad types), which is why modern PHP feels stricter. But fatal errors - out of memory, timeouts - bypass error handlers entirely. The companion tool:
register_shutdown_function(function () {
$e = error_get_last();
if ($e && in_array($e['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
error_log(json_encode(['fatal' => $e])); // last-words logging
// fastcgi users: a clean 500 page can be emitted here too
}
});
Handler for the catchable, shutdown function for the last words - together they mean nothing dies silently. (This shutdown hook shares real estate with post-response processing - same mechanism, different jobs.)
Two closing notes: don't heap logic into the error handler itself (convert and get out - handlers that query databases create spectacular failure loops), and in tests, PHPUnit does this conversion for you, which is why code "only fails in tests" - the tests are telling the truth. Production deserves the same honesty.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.