SOLARISE
DEV

Quickly & Reliably Handle Errors in PHP Code

Robust error handling for cleaner PHP code

Originally published: December 16th, 2017. Updated on: April 29th, 2025.

When you’re building something in PHP, whether a simple website or a larger web application, you always need to be on the lookout for errors cropping up. Nasty little bugs can creep in when you least expect them.

An error happens whenever PHP tries to execute an instruction that results in an impossible outcome or otherwise prevents the script from running as intended. Errors range from simple fatal errors (halting execution, often due to syntax mistakes or calling non-existent functions) to more complex issues where the cause isn't immediately clear, requiring an in-depth code review.

Undefined Variables & Other Little Glitches

Dealing with errors gracefully is a major consideration in PHP development. It’s important that when something goes wrong (and it likely will, eventually), your users aren’t greeted with an unsightly page of technical jargon.

PHP's default error output is often simple text with basic formatting – quite ugly, really.

<?php
// Trying to use a variable that hasn't been defined
echo ($hello_world);
?>

This might produce output like:

Notice: Undefined variable: hello_world in C:\path\to\your\php.php on line 5

You might not see this right away if your PHP configuration isn't set to display all errors. To enable full error reporting for development:

<?php
// Report all errors, including strict standards
error_reporting(E_ALL);
// Display errors directly on screen (DISABLE ON PRODUCTION!)
ini_set('display_errors', 1); // Use 1 for TRUE

echo ($hello_world);
?>

The first line tells PHP to report everything. The second line instructs PHP to display errors directly (this value is also in php.ini, but setting it here is useful if you can't access php.ini). Remember to turn display_errors OFF on live production servers!

One way to avoid the "Undefined variable" notice is to check if a variable exists before using it, often with isset():

<?php
// Check if $hello_world is set before echoing
if (isset($hello_world)) {
    echo $hello_world;
} else {
    echo "The variable 'hello_world' couldn't be located.";
}
?>

But doing this for every variable isn't practical, and the custom message still reveals an internal issue.

Ugly Errors? Prettify Them With Custom Error Handling

A common fatal error message looks like this:

Fatal error: Call to undefined function hello_world() in C:\path\to\your\php.php on line 1

This isn't pretty. A visitor seeing this would likely be discouraged. A better approach is using a custom error handler. This lets you intercept PHP's default error handling and replace it with your own logic – perhaps showing a user-friendly message, logging the error for yourself, or both.

You create a custom error handler function that accepts up to five arguments:

  1. $level (int, required): The error level (e.g., E_WARNING, E_NOTICE).
  2. $message (string, required): The error message.
  3. $file (string, optional): The filename where the error occurred.
  4. $line (int, optional): The line number where the error occurred.
  5. $context (array, optional): An array of all variables existing in scope when the error occurred (use with caution).

Common error levels include:

  • E_WARNING (2): Non-fatal runtime warning. Script continues.
  • E_NOTICE (8): Runtime notice. Might indicate an error or something normal during script execution. Script continues.
  • E_USER_ERROR (256): Fatal user-generated error (triggered by trigger_error()). Halts script unless handled.
  • E_USER_WARNING (512): Non-fatal user-generated warning. Script continues.
  • E_USER_NOTICE (1024): User-generated notice. Script continues.
  • E_RECOVERABLE_ERROR (4096): Catchable fatal error. Can potentially be handled by the error handler (e.g., to log it) or using try...catch with Exceptions (covered later).
  • E_DEPRECATED (8192): Runtime notice about using deprecated features.
  • E_USER_DEPRECATED (16384): User-generated deprecation notice.

Let's create a simple custom error function:

<?php
/**
    * Custom error handler function.
    *
    * @param int    $level   The error level.
    * @param string $message The error message.
    * @param string $file    The file where the error occurred.
    * @param int    $line    The line number where the error occurred.
    * @return bool True to prevent default PHP error handler, false otherwise.
    */
function myCustomErrorHandler(int $level, string $message, string $file = '', int $line = 0): bool
{
    // Note: The final three arguments are optional in the function signature
    // but are usually provided by PHP when the handler is called.

    // Simple user-friendly message
    echo "Oops! Something went wrong on our end.<br>";
    echo "Please contact the site administrator at admin@example.com and let them know.<br>";

    // Optionally, log the detailed error for the developer (see next section)
    // error_log("PHP Error [$level]: $message in $file on line $line");

    // For notices and warnings, we might let PHP handle them if needed,
    // but for this example, we'll handle everything and prevent default handling.
    // Returning true prevents the standard PHP error handler from running.
    return true;
}

// Set our function as the default error handler
set_error_handler("myCustomErrorHandler");

// Trigger the undefined variable notice again
echo ($hello_world);
?>

This now produces a friendlier message for the user:

Oops! Something went wrong on our end. Please contact the site administrator at admin@example.com and let them know.

This is better, but ideally, visitors shouldn't see any error messages. We want to shield them while getting detailed information for ourselves.

Logging Errors with error_log()

PHP's error_log() function is invaluable for recording error details without displaying them to users. You can log errors to the system logger, send them via email, or write them to a file.

Let's modify our handler to log errors to a file:

<?php
function myCustomErrorHandlerWithLogging(int $level, string $message, string $file = '', int $line = 0): bool
{
    // Define the path to your log file
    $log_file = __DIR__ . '/php_errors.log'; // Log in the same directory as this script

    // Format the log message
    $error_message = sprintf(
        "[%s] PHP Error Level %d: %s in %s on line %d\n",
        date("Y-m-d H:i:s"), // Add timestamp
        $level,
        $message,
        $file,
        $line
    );

    // Log the error to the specified file (message type 3)
    // Ensure the web server has write permissions for this file/directory!
    error_log($error_message, 3, $log_file);

    // Optionally, display a generic error message to the user ONLY for critical errors
    if ($level === E_ERROR || $level === E_USER_ERROR || $level === E_RECOVERABLE_ERROR) {
            echo "Sorry, a critical error occurred. We've been notified and are looking into it.";
    }

    // Prevent default PHP error handler
    return true;
}

// Set the new handler
set_error_handler("myCustomErrorHandlerWithLogging");

// Trigger the error again
echo ($hello_world); // This will now be logged

// Trigger a user error
trigger_error("This is a custom user warning", E_USER_WARNING); // Also logged

// Trigger a fatal user error
// trigger_error("This is a fatal user error", E_USER_ERROR); // This would halt script if not handled
?>

Now, when $hello_world is accessed, nothing appears on screen (unless it's a critical error type as defined in the if statement), but the details are appended to php_errors.log:

[2025-04-29 10:12:00] PHP Error Level 8: Undefined variable $hello_world in /path/to/your/script.php on line X [2025-04-29 10:12:00] PHP Error Level 512: This is a custom user warning in /path/to/your/script.php on line Y

Important: Logging every notice (like undefined variables) can be resource-intensive on busy sites. Consider logging only more severe errors in production.

Selective Error Handling

You can make your handler smarter by acting differently based on the error level:

<?php
function selectiveErrorHandler(int $level, string $message, string $file = '', int $line = 0): bool
{
    $log_file = __DIR__ . '/php_errors.log';
    $error_message = sprintf(
        "[%s] PHP Error Level %d: %s in %s on line %d\n",
        date("Y-m-d H:i:s"), $level, $message, $file, $line
    );

    switch ($level) {
        case E_ERROR:
        case E_USER_ERROR:
        case E_RECOVERABLE_ERROR:
            // Log critical errors and show generic message
            error_log($error_message, 3, $log_file);
            echo "A critical error occurred. Please try again later.";
            // Optionally exit for fatal errors if needed, though set_error_handler
            // might not always prevent shutdown for true E_ERROR.
            // exit(1);
            break;

        case E_WARNING:
        case E_USER_WARNING:
            // Log warnings, but don't show user message
            error_log($error_message, 3, $log_file);
            break;

        case E_NOTICE:
        case E_USER_NOTICE:
        case E_DEPRECATED:
        case E_USER_DEPRECATED:
            // Log notices/deprecated only if specifically enabled for logging
            // Or just ignore them in production
            // error_log($error_message, 3, $log_file);
            break;

        default:
            // Log unknown error types
            error_log($error_message, 3, $log_file);
            break;
    }

    // Prevent default PHP error handler for handled types
    return true;
}

set_error_handler("selectiveErrorHandler");

// Example: Trigger a user notice (might be ignored by the handler above)
trigger_error("Just an informational notice", E_USER_NOTICE);
?>

Fatal Errors: Remember, set_error_handler cannot reliably handle all fatal errors (like E_ERROR from syntax mistakes or calling truly undefined functions). These often halt the script before the handler can fully execute or prevent shutdown. Using register_shutdown_function() is another technique to catch some fatal errors for logging purposes, but proper coding and testing are the best prevention.

Introducing: Debug Mode

During development, seeing errors immediately is useful. But you don't want visitors seeing them. You can create a "debug mode" for your error handler.

Debug Using Your IP Address

You can show detailed errors only if the request comes from your specific IP address.

<?php
function ipBasedErrorHandler(int $level, string $message, string $file = '', int $line = 0): bool
{
    $developer_ip = "YOUR_IP_ADDRESS_HERE"; // Replace with your actual IP
    $log_file = __DIR__ . '/php_errors.log';
    $error_message_log = sprintf( /* ... as before ... */ );
    $error_message_display = "PHP Error [$level]: $message in $file on line $line";

    // Check if the request IP matches the developer's IP
    if (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] === $developer_ip) {
        // Show detailed error to the developer
        echo "<pre>" . htmlspecialchars($error_message_display) . "</pre>";
    } else {
        // Log critical errors for normal users
        if ($level >= E_USER_ERROR) { // Example threshold
            error_log($error_message_log, 3, $log_file);
            // Optionally show generic message
            // echo "An error occurred.";
        }
    }
    return true;
}

set_error_handler("ipBasedErrorHandler");

// Get your IP (be careful with proxies/load balancers)
// echo "Your IP: " . $_SERVER['REMOTE_ADDR'];

echo ($hello_world);
?>

Note: Getting the correct visitor IP can be tricky behind proxies or load balancers ($_SERVER['HTTP_X_FORWARDED_FOR'] might be needed). This method is less reliable if your IP changes often.

Debug Using $_GET Variables

A more flexible way is to use a URL parameter (e.g., ?debug=1).

<?php
function getBasedErrorHandler(int $level, string $message, string $file = '', int $line = 0): bool
{
    $is_debug_mode = isset($_GET['debug']) && $_GET['debug'] === '1';
    $log_file = __DIR__ . '/php_errors.log';
    $error_message_log = sprintf( /* ... as before ... */ );
    $error_message_display = "PHP Error [$level]: $message in $file on line $line";

    if ($is_debug_mode) {
        // Show detailed error if debug mode is on
        echo "<pre>" . htmlspecialchars($error_message_display) . "</pre>";
    } else {
        // Log critical errors for normal users
        if ($level >= E_USER_ERROR) { // Example threshold
            error_log($error_message_log, 3, $log_file);
        }
    }
    return true;
}

set_error_handler("getBasedErrorHandler");

echo ($hello_world);
?>

Now, visit yourscript.php normally to see no error (but it gets logged if critical). Visit yourscript.php?debug=1 to see the detailed error message directly. This makes it easy to switch between developer view and user view.

Taking An Exception: A More Modern Approach

While custom error handlers are useful, modern PHP often favors Exceptions for handling errors, especially within object-oriented code. Exceptions provide a more structured way to deal with errors and allow for graceful recovery.

An Exception is an object representing an error or unexpected event. When an exceptional situation occurs, code can throw an Exception. This immediately stops normal execution flow and looks for a catch block that can handle that type of Exception.

Using try...catch for Runtime Errors

Let's rewrite a function to throw an Exception if input is invalid:

<?php
/**
    * Example function that throws an exception on invalid input.
    *
    * @param int $v Value to check.
    * @return int The value multiplied by 2 if valid.
    * @throws InvalidArgumentException If value is not between 1 and 6.
    */
function checkValue(int $v): int
{
    if ($v < 1 || $v > 6) {
    // Throw a specific type of exception
    throw new InvalidArgumentException("The value ({$v}) is not between 1 and 6 inclusive.");
    }
    return $v * 2;
}

try {
    echo "Trying value 4: " . checkValue(4) . "<br>"; // This works
    echo "Trying value 10: " . checkValue(10) . "<br>"; // This throws an exception
    echo "This line will not be reached if an exception is thrown above.<br>";
} catch (InvalidArgumentException $e) {
    // Catch the specific exception type
    echo "Caught Exception: " . htmlspecialchars($e->getMessage()) . "<br>";
    // $e object contains more info: getFile(), getLine(), getTraceAsString() etc.
    // Log the error here if needed: error_log($e);
} catch (Exception $e) {
    // Catch any other general Exception types (optional fallback)
    echo "Caught generic Exception: " . htmlspecialchars($e->getMessage()) . "<br>";
} finally {
    // This block executes regardless of whether an exception was caught or not
    echo "Execution finished.<br>";
}

echo "Script continues after try-catch block.<br>";
?>

Output:

Trying value 4: 8
Caught Exception: The value (10) is not between 1 and 6 inclusive.
Execution finished.
Script continues after try-catch block.

Explanation:

  • The try block contains code that might throw an Exception.
  • When checkValue(10) is called, it throws an InvalidArgumentException.
  • Execution jumps immediately to the catch (InvalidArgumentException $e) block.
  • The code inside the catch block runs, accessing the Exception object ($e) to get the message.
  • The finally block runs afterwards.
  • Execution then continues after the try...catch...finally structure.

Using specific Exception types (like InvalidArgumentException, RuntimeException, TypeError, or custom ones you define) allows for more granular error handling.

Using Exceptions in Classes

Exceptions are particularly useful in classes to signal errors deep within methods. The try...catch block can be placed higher up in the call stack to handle errors originating from objects.

<?php
class Calculator
{
    public function divide(float $a, float $b): float
    {
        if ($b == 0) {
            throw new DivisionByZeroError("Cannot divide by zero."); // Specific Error type
        }
        if ($a < 0 || $b < 0) {
                throw new InvalidArgumentException("Only positive numbers allowed.");
        }
        return $a / $b;
    }
}

$calc = new Calculator();

try {
    echo "10 / 2 = " . $calc->divide(10, 2) . "<br>";
    // echo "10 / 0 = " . $calc->divide(10, 0) . "<br>"; // Throws DivisionByZeroError
    echo "-10 / 2 = " . $calc->divide(-10, 2) . "<br>"; // Throws InvalidArgumentException

} catch (DivisionByZeroError $e) {
    echo "Error: " . htmlspecialchars($e->getMessage()) . "<br>";
    // Log specifically: error_log("Division by zero attempt: " . $e);
} catch (InvalidArgumentException $e) {
        echo "Input Error: " . htmlspecialchars($e->getMessage()) . "<br>";
        // Log specifically: error_log("Invalid argument: " . $e);
} catch (Throwable $e) { // Catch any remaining Error or Exception (PHP 7+)
    echo "An unexpected error occurred: " . htmlspecialchars($e->getMessage()) . "<br>";
    // Generic logging: error_log("Unexpected Throwable: " . $e);
}

echo "Calculation attempt finished.<br>";
?>

The Exception/Throwable Object ($e)

The caught object ($e in the examples, an instance of Exception or Throwable in PHP 7+) provides useful methods:

  • getMessage(): Gets the error message string.
  • getCode(): Gets the error code (often 0 unless specified when thrown).
  • getFile(): Gets the file where the exception was thrown.
  • getLine(): Gets the line number where the exception was thrown.
  • getTrace(): Returns an array representing the call stack leading up to the exception.
  • getTraceAsString(): Returns the call stack trace as a formatted string.
  • getPrevious(): Gets the previous Exception if it was chained.
  • __toString(): Often provides a formatted string representation of the exception including trace.

Using var_dump($e->getTrace()) or echo $e->getTraceAsString(); inside a catch block is invaluable for debugging.

Combining Approaches

You can combine set_error_handler and Exceptions. Your error handler can be set to convert certain PHP errors (like Warnings or Notices) into ErrorException objects, which can then be caught by try...catch blocks. This allows for more consistent error handling logic.

<?php
// Error handler that throws ErrorException for warnings/notices
set_error_handler(function(int $level, string $message, string $file = '', int $line = 0) {
    // Throw only for error levels included in error_reporting
    if (!(error_reporting() & $level)) {
        return false; // Respect error_reporting level
    }
    throw new ErrorException($message, 0, $level, $file, $line);
});

error_reporting(E_ALL); // Report everything

try {
    echo $undefined_variable; // This Notice will be caught as an ErrorException
} catch (ErrorException $e) {
    echo "Caught ErrorException: " . htmlspecialchars($e->getMessage());
    echo " Severity: " . $e->getSeverity(); // E_NOTICE (8)
        // Log $e here...
}

restore_error_handler(); // Restore previous handler when done
?>

A Summary

Handling errors effectively is crucial for building reliable PHP applications.

  • Use error_reporting(E_ALL) and ini_set('display_errors', 1) during development.
  • Use ini_set('display_errors', 0) and ini_set('log_errors', 1) in production.
  • A custom error handler (set_error_handler) allows you to control how errors are presented or logged, shielding users from raw PHP errors.
  • Exceptions (try...catch, throw) provide a structured, object-oriented way to handle exceptional conditions, especially useful in complex applications and classes.
  • Combining these techniques (e.g., converting errors to exceptions) can create a robust error handling strategy.

Investing time in solid error handling saves countless hours of debugging later and ensures a better experience for your users, even when things inevitably go wrong behind the scenes. It's just good practice.

Robin Metcalfe

About the Author: Robin Metcalfe

Robin is a freelance web strategist and developer based in Edinburgh, with over 15 years of experience helping businesses build effective and engaging online platforms using technologies like Laravel and WordPress.

Get in Touch