<< Home

Chapter 11: Errors and Exceptions

PHP distinguishes between errors and exceptions. Errors signal runtime problems in code or environment; exceptions represent controllable failures that you can catch and handle. This chapter explores error levels, the Throwable interface, structured exception handling, custom exception types, user error handlers, and practical logging strategies.

Error Levels and Reporting

PHP classifies errors by severity and behavior. Some issues are recoverable; others halt execution. You can configure reporting to suit development or production needs. Controlling error levels ensures useful diagnostics without leaking sensitive details.

Understanding PHP error types

Error levels include fatal errors, warnings, notices, and deprecations. Modern PHP represents all errors as Error exceptions that implement Throwable, but legacy code may still emit traditional messages.

LevelDescription
E_ERRORFatal error that stops execution
E_WARNINGNon fatal issue that continues execution
E_NOTICEInformational message about questionable code
E_DEPRECATEDUse of a feature that will be removed
E_PARSECompile time parse error

Configuring error_reporting() and display settings

Use error_reporting() to control which levels PHP reports, and set display_errors and log_errors in php.ini or at runtime. During development show everything; in production hide output and log instead.

// Show all errors during development
error_reporting(E_ALL);
ini_set("display_errors", "1");

// Hide errors but log in production
error_reporting(E_ALL);
ini_set("display_errors", "0");
ini_set("log_errors", "1");
ini_set("error_log", "/var/log/php_errors.log");
💡 Always test production builds with display disabled. Unexpected error output can break JSON or HTML responses.

Exceptions and Throwable

Exceptions are objects that represent failures during program execution. They implement the Throwable interface, which unifies Error and Exception under a single hierarchy. Throwing and catching exceptions lets you separate error handling from normal logic.

Throwing and catching basic exceptions

Use the throw keyword to signal an error and try...catch to intercept it. PHP unwinds the call stack until it finds a matching catch block or terminates with a fatal error.

function divide(float $a, float $b): float {
  if ($b === 0.0) {
    throw new Exception("Division by zero");
  }
  return $a / $b;
}

try {
  echo divide(4, 0);
} catch (Exception $e) {
  echo "Error: " . $e->getMessage();
}

Understanding the Throwable interface

Both Error and Exception implement Throwable, which provides methods such as getMessage(), getCode(), getFile(), and getTrace(). This means you can catch Throwable to handle both kinds uniformly.

try {
  undefined_function();
} catch (Throwable $t) {
  echo "Caught: " . $t->getMessage();
}
⚠️ Catching Throwable is useful in top level error handlers but can obscure root causes if overused. Catch more specific types where possible.

try, catch, finally

The try block wraps code that might throw; catch handles exceptions; finally executes regardless of outcome. Use finally for cleanup such as releasing resources or closing files.

Using multiple catch blocks

Catch blocks are tested in order. Each may specify a different type. The first that matches handles the exception.

try {
  risky();
} catch (InvalidArgumentException $e) {
  echo "Invalid argument: " . $e->getMessage();
} catch (RuntimeException $e) {
  echo "Runtime problem: " . $e->getMessage();
} finally {
  echo "Done.";
}

Using union catch syntax

PHP allows multiple types in one catch block with a pipe separator. This is convenient for related errors that share handling logic.

try {
  risky();
} catch (LogicException | RuntimeException $e) {
  echo "Caught logic or runtime issue: " . $e->getMessage();
}

Re-throwing and wrapping exceptions

You can re-throw an exception with throw or create a new one that wraps the previous via the previous parameter. This preserves the original trace while adding context.

try {
  risky();
} catch (Exception $e) {
  throw new RuntimeException("Higher level failure", 0, $e);
}
💡 Always preserve the previous exception when wrapping. It saves hours of debugging later.

Custom Exception Types

Defining custom exception classes clarifies intent and allows fine-grained handling. They can extend Exception or one of its descendants, adding context or structured data.

Creating domain specific exceptions

Keep exception names descriptive and hierarchical. Group related exceptions under a shared base type.

namespace App\Exception;

class AppException extends \Exception {}
class ValidationException extends AppException {}
class DatabaseException extends AppException {}

Adding extra data to custom exceptions

You can include additional properties or methods that describe the failure more precisely.

class HttpException extends Exception {
  public function __construct(
    private int $status,
    string $message = "",
    ?Throwable $previous = null
  ) {
    parent::__construct($message, $status, $previous);
  }

  public function getStatus(): int {
    return $this->status;
  }
}
throw new HttpException(404, "Not Found");
⚠️ Keep custom exceptions lightweight. They should describe failure, not perform logic. Complex handling belongs in catch blocks or dedicated handlers.

Error Handlers and set_error_handler()

The error handler converts traditional errors into exceptions or logs them centrally. You can register a custom function with set_error_handler() to intercept PHP errors before default handling.

Registering a basic error handler

The handler receives the level, message, file, and line. Returning true prevents PHP’s internal handler from running. Converting errors to ErrorException simplifies unified exception handling.

set_error_handler(function(
  int $level,
  string $message,
  string $file,
  int $line
) {
  throw new ErrorException($message, 0, $level, $file, $line);
});

Restoring and suppressing handlers

Use restore_error_handler() to revert to the previous handler. The @ operator suppresses errors for a specific expression, though it is best avoided since it hides useful information.

@unlink("/nonexistent/file.txt");  // Suppresses warning
restore_error_handler();

Handling fatal errors with register_shutdown_function()

Shutdown functions can inspect error_get_last() to detect fatal errors that escaped earlier handling.

register_shutdown_function(function() {
  $error = error_get_last();
  if ($error && $error["type"] === E_ERROR) {
    error_log("Fatal: " . $error["message"]);
  }
});
💡 Centralize error handling early in the request lifecycle. Frameworks often do this for you, but standalone scripts benefit just as much.

Logging Strategies

Logging preserves diagnostic information without showing it to users. PHP’s error_log() function writes messages to the configured log, and PSR-3 loggers offer structured approaches suitable for larger systems.

Using error_log() for simple logging

This function writes to the system log or a configured file. Supply a custom message type to send via email or socket if required.

error_log("Cache miss for key=$key");

Integrating a PSR-3 compatible logger

Libraries such as Monolog implement the Psr\Log\LoggerInterface. You can inject a logger and use levels like info, warning, or error consistently across your codebase.

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$logger = new Logger("app");
$logger->pushHandler(new StreamHandler(__DIR__ . "/app.log", Logger::INFO));

$logger->info("User login", ["user" => $id]);
$logger->error("Exception caught", ["exception" => $e]);

Structuring and rotating logs

Adopt a consistent format such as JSON lines for easy ingestion. Use rotation tools or Monolog’s RotatingFileHandler to prevent large files. Include request identifiers or correlation IDs for tracing distributed operations.

$logger->pushHandler(new \Monolog\Handler\RotatingFileHandler(
  __DIR__ . "/logs/app.log",
  7
));
⚠️ Logs can contain sensitive data. Sanitize user input and avoid dumping full payloads in production environments.