Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

Turn PHP Warnings into Exceptions (and Catch Them Like an Adult)

C
Carlo Fontanos
· 2 min read

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.

C
Written by Carlo Fontanos

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.

Message sent!

Your details are only used to reply to you.

Keep reading