PHP Error Handling Best Practices for Maintainable Code

Backend2026-09-25TryQuickToolBox

You've just deployed a new feature, and suddenly your PHP application throws a fatal error. Users see a blank page, and your logs are empty. Sound familiar? Poor error handling is one of the most common reasons PHP code becomes unmaintainable. In this article, we'll walk through practical PHP error handling best practices that will make your code more robust, easier to debug, and simpler to maintain.

Why PHP Error Handling Matters

PHP is forgiving by default: it often continues execution after a warning, and errors can be silently ignored. This flexibility is a double-edged sword. Without proper handling, errors can:

Good error handling ensures that when something goes wrong, you know about it, you can fix it quickly, and your users have a graceful experience.

1. Set Appropriate Error Reporting Levels

The first step is to configure PHP's error reporting correctly for your environment. In development, you want to see all errors; in production, you want to log them but not display them.

// Development
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

// Production
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', '/path/to/php-error.log');
error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);

Use environment variables or configuration files to switch between these settings automatically. Never rely on manually editing php.ini on production servers.

2. Use Exceptions Instead of Error Codes

Returning error codes (like false or -1) is a legacy pattern that clutters your code and makes it easy to ignore failures. Exceptions force you to handle errors explicitly and keep your happy path clean.

// Bad: error code
function getUser($id) {
    $user = db_find($id);
    if (!$user) {
        return false; // caller must check
    }
    return $user;
}

// Good: exception
function getUser($id) {
    $user = db_find($id);
    if (!$user) {
        throw new UserNotFoundException("User $id not found");
    }
    return $user;
}

Create custom exception classes for different error types. This makes it easy to catch specific errors and handle them appropriately.

3. Catch Exceptions at the Right Level

A common mistake is catching exceptions too early or too broadly. Catch exceptions only when you can actually do something about them—log them, retry, or show a user-friendly message.

try {
    $user = getUser($id);
    $order = createOrder($user, $items);
} catch (UserNotFoundException $e) {
    // Handle missing user specifically
    return response('User not found', 404);
} catch (PaymentFailedException $e) {
    // Handle payment failure
    return response('Payment failed: ' . $e->getMessage(), 400);
} catch (Throwable $e) {
    // Catch-all for unexpected errors
    log_error($e);
    return response('Something went wrong', 500);
}

Use Throwable (PHP 7+) to catch both exceptions and errors. Avoid empty catch blocks—if you catch, do something meaningful.

4. Log Errors with Context

Logging is your best friend for debugging production issues. But a log message like "Error occurred" is useless. Include context: user ID, request parameters, stack trace, and timestamps.

try {
    processPayment($order);
} catch (PaymentException $e) {
    error_log(sprintf(
        "Payment failed for order %d: %s in %s:%d\nStack trace: %s",
        $order->id,
        $e->getMessage(),
        $e->getFile(),
        $e->getLine(),
        $e->getTraceAsString()
    ));
    throw $e; // re-throw after logging
}

Consider using a logging library like Monolog for structured logs. It supports different handlers (file, syslog, Slack) and log levels (debug, info, warning, error).

5. Create a Custom Error Handler

PHP's default error handler prints errors to the screen, which is not suitable for production. A custom error handler lets you convert errors to exceptions, log them, or display a friendly error page.

set_error_handler(function ($severity, $message, $file, $line) {
    if (!(error_reporting() & $severity)) {
        return false; // respect error_reporting settings
    }
    throw new ErrorException($message, 0, $severity, $file, $line);
});

set_exception_handler(function ($e) {
    log_error($e);
    http_response_code(500);
    include 'views/error.php';
});

register_shutdown_function(function () {
    $error = error_get_last();
    if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
        log_error(new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line']));
    }
});

This setup ensures that all errors—including fatal ones—are logged and handled gracefully.

6. Validate Input and Fail Early

Many errors stem from invalid input. Validate data at the boundaries of your application (controllers, API endpoints) and throw exceptions immediately if validation fails. This prevents errors from propagating deep into your code.

function createUser(array $data) {
    if (empty($data['email'])) {
        throw new InvalidArgumentException('Email is required');
    }
    if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
        throw new InvalidArgumentException('Invalid email format');
    }
    // ... proceed with confidence
}

Use PHP's filter functions or a validation library (like Respect\Validation) to keep validation consistent.

7. Don't Suppress Errors with @

The @ operator silences errors, making debugging harder. It's tempting to use it when calling functions that might emit warnings (like file_get_contents), but it hides real problems. Instead, check preconditions or use try-catch with exceptions.

// Bad
$content = @file_get_contents($url);

// Good
if (!is_readable($url)) {
    throw new RuntimeException("Cannot read $url");
}
$content = file_get_contents($url);

If you must suppress, do it only for well-understood cases and document why.

8. Use a Centralized Error Handling Middleware

In frameworks like Laravel or Symfony, error handling is often centralized in middleware or an exception handler. If you're building your own, create a single entry point that catches all exceptions and converts them to HTTP responses.

// In your front controller (index.php)
try {
    $response = $router->dispatch($request);
} catch (HttpException $e) {
    $response = new Response($e->getMessage(), $e->getStatusCode());
} catch (Throwable $e) {
    log_error($e);
    $response = new Response('Internal Server Error', 500);
}
$response->send();

This keeps error handling logic in one place and ensures consistency.

Comparison: Error Handling Approaches

Approach Pros Cons
Error codes Simple, no exceptions Easy to ignore, clutters code
Exceptions Forces handling, clean separation Can be overused, performance overhead
Custom error handler Centralized, catches all errors Requires setup, can mask errors if misconfigured
Logging only Non-intrusive, good for monitoring Doesn't handle errors, only records

FAQ

What's the difference between errors and exceptions in PHP?

Errors are low-level issues like syntax errors or type errors, while exceptions are thrown objects that represent exceptional conditions. In PHP 7+, both implement the Throwable interface, so you can catch both with a single catch block.

Should I use try-catch for every function call?

No, that leads to overly defensive code. Catch exceptions only when you can handle them meaningfully—log, retry, or show a user-friendly message. Let exceptions bubble up to a central handler for unexpected cases.

How do I log errors without exposing sensitive data?

Sanitize log messages by removing passwords, tokens, and personal data. Use structured logging with context fields, and configure your logging library to redact sensitive keys. Also, ensure log files are stored securely and access is restricted.

Ready to streamline your PHP error handling? Start by auditing your current error reporting settings and implementing a custom error handler. For quick debugging of JSON payloads or logs, try our JSON Formatter to validate and pretty-print your data.