Discover best practices for modern PHP error handling and logging. Transition from legacy methods to secure, efficient error management with custom handlers and PSR-3 compliant logging.
An uncaught error at 2am tells you two things: what broke, and how badly your logging let you down. If the answer is a white screen and a truncated line in a file nobody remembers writing, you’re debugging blind.
Legacy PHP is full of that. Silent failures, errors hidden behind the @ operator, and display_errors leaking stack traces straight to visitors. This walks through bringing an old codebase up to modern error handling and logging: from the built-in functions, through catchable errors in PHP 7 and up, to structured logging with Monolog.
Table of Contents
- Why Modernize Error Handling in PHP?
- Basic Error Handling in PHP
- Advanced Error Handling Strategies
- Implementing Custom Error Handlers
- Best Practices for Logging in PHP
- Implementing PSR-3 Compliant Logging
- Conclusion
Why Modernize Error Handling in PHP?
Old PHP code leaned on error_reporting() and scattered try/catch blocks, when it bothered at all. The language has moved on. The biggest shift landed in PHP 7: most fatal errors that used to kill the script are now thrown as Error objects, and both Error and Exception implement the Throwable interface. So you can catch a type error or a call to an undefined method the same way you catch an exception, instead of watching the process die (see php.net, “Errors in PHP 7”). Modernizing buys you three things: you catch problems early, you stop silent failures, and you get diagnostics you can actually read.
Basic Error Handling in PHP
PHP ships with the basics: error_reporting(), trigger_error(), and set_error_handler(). Here’s a custom handler wired up with set_error_handler():
<?php
/**
* Custom error handler function.
*
* @param int $errno The level of the error raised.
* @param string $errstr The error message.
* @param string $errfile The filename where the error was raised.
* @param int $errline The line number where the error was raised.
*
* @return bool True if the error has been handled.
*/
function custom_error_handler( $errno, $errstr, $errfile, $errline ) {
echo "Error [$errno]: $errstr in $errfile on line $errline";
return true;
}
// Set custom error handler.
set_error_handler( 'custom_error_handler' );
// Trigger an error.
echo $undefined_variable;
It works, but it doesn’t scale, and it’s important to know its limit: set_error_handler() only catches the classic PHP errors like warnings and notices, not exceptions and not fatal errors. For real applications you’ll lean on exceptions, and if you’re on a framework, the handling Symfony or Laravel give you out of the box.
Advanced Error Handling Strategies
Once exceptions are in play, a few techniques keep error handling clean across the layers of an app: custom exception classes, one place that logs, and letting errors propagate up to the layer that can actually deal with them.
1. Using Custom Exception Classes
A custom exception class lets you attach meaning to a specific failure. A database problem and a missing file are not the same event, so give them their own types:
<?php
/**
* Custom DatabaseException class.
*/
class DatabaseException extends Exception {
/**
* Custom error message for database errors.
*
* @return string Error message.
*/
public function error_message() {
return "Database error on line {$this->getLine()} in {$this->getFile()}: {$this->getMessage()}";
}
}
try {
// Simulate a database error.
throw new DatabaseException( 'Unable to connect to the database.' );
} catch ( DatabaseException $e ) {
echo $e->error_message();
}Now each failure carries its own context and message, which makes both the catch site and the log line clearer.
2. Catching Multiple Exceptions
You can stack catch blocks to handle different types differently:
<?php
try {
// Some code that may throw different types of exceptions.
} catch ( DatabaseException $e ) {
// Handle database-specific errors.
} catch ( FileNotFoundException $e ) {
// Handle file-specific errors.
} catch ( Exception $e ) {
// Handle generic errors.
}
Since PHP 7.1 you can also group types in one block with the pipe syntax, catch ( DatabaseException | FileNotFoundException $e ), when the handling is identical (php.net, “Catching multiple exception types”). Match the handler to the failure, and let anything you can’t handle here bubble up.
Implementing Custom Error Handlers
A custom handler can log somewhere useful and show users something friendly instead of a stack trace. But one handler won’t cover everything PHP throws at you. Three functions together do:
set_error_handler()for non-fatal errors like warnings and notices.set_exception_handler()for uncaught exceptions.register_shutdown_function(), paired witherror_get_last(), to catch fatal errors that slip past the other two.
Here’s a handler that logs to a file:
<?php
/**
* Custom error handler to log errors to a file.
*
* @param int $errno The level of the error raised.
* @param string $errstr The error message.
* @param string $errfile The filename where the error was raised.
* @param int $errline The line number where the error was raised.
*
* @return bool True if the error has been handled.
*/
function log_error_to_file( $errno, $errstr, $errfile, $errline ) {
$log_message = "[$errno] $errstr in $errfile on line $errline" . PHP_EOL;
file_put_contents( 'error_log.txt', $log_message, FILE_APPEND );
return true;
}
// Set the custom error handler.
set_error_handler( 'log_error_to_file' );
// Trigger an error.
echo $undefined_variable;
That gets errors onto disk for later. In production, don’t stop there: set display_errors to Off so nothing leaks to visitors, and log_errors to On so everything lands in your log (php.net, “Runtime configuration”).
Best Practices for Logging in PHP
Good logging lets you watch an app in real time, spot slowdowns, and track down bugs fast. A few rules that hold up:
1. Log to Files and Remote Systems
The built-in error_log() function is the simplest step up from echo: it writes a message to whatever your log_errors configuration points at. Local logs are a fine start, but shipping them to an external system, a log server, a database, or a platform like Sentry or Graylog, scales better and gives you real-time tracking across servers.
2. Use Log Levels
Tag each message with a severity so you can filter to what matters. PSR-3 standardizes eight levels, borrowed from RFC 5424: debug, INFO, notice, WARNING, ERROR, CRITICAL, alert, and emergency. That turns “show me only errors and worse” into a real query instead of a grep guess.
3. Avoid Logging Sensitive Information
Keep passwords, tokens, and personally identifiable information (PII) out of your logs. A log file is just another place data can leak from, so treat it like one.
Implementing PSR-3 Compliant Logging
PSR-3 is the shared interface for logging in PHP. Code against it and you can swap logging libraries without touching your call sites. Monolog is the usual choice, and it implements PSR-3:
<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
// Create a logger instance.
$log = new Logger( 'name' );
// Define where to store the logs (file).
$log->pushHandler( new StreamHandler( 'path/to/your.log', Logger::WARNING ) );
// Add log entries.
$log->warning( 'This is a warning message.' );
$log->error( 'This is an error message.' );
Monolog can write to files, databases, Slack, or a monitoring service, and you can stack several handlers on one logger. One note if you’re on Monolog 3: the level constants shown above moved to the Monolog\Level enum (for example Level::Warning), so check which major version you’ve pulled in (Monolog docs).
Conclusion
Bringing legacy PHP up to modern error handling isn’t busywork. Catchable errors, typed exceptions, layered handlers, and PSR-3 logging turn “the site is down and I don’t know why” into a log line you can read. Start where it hurts most, show visitors nothing, log everything you can act on, and keep secrets out of the logs. The reliability you get back is worth the afternoon it takes.


