Skip to content

PHP errors, exceptions, and shutdown behavior

Status: Complete. Last reviewed 2026-08-28.

PHP exposes several failure channels: thrown Throwable objects, diagnostic error levels handled by the engine, process termination, and ordinary domain alternatives. A robust boundary identifies which channel it receives, whether execution can safely continue, what evidence must be recorded, and who owns the final response or exit code.

Throwable is the common interface for values that can be thrown and caught. Exception is the base for application and many library exceptions. Error represents engine-detected failures that modern PHP exposes as throwable objects, including TypeError, ValueError, ArgumentCountError, ParseError, and several arithmetic/assertion failures.

An Error being catchable does not make the condition recoverable. Catching TypeError around an entire request to return a controlled response may be reasonable; continuing a multi-step mutation after a programming-contract violation often is not. Catch the narrowest failure at the layer that can add context, compensate, translate it into a stable boundary result, or make a justified retry decision.

User exceptions should usually encode a meaningful failure category, not one class per message string. Preserve the original as previous when translating infrastructure detail into an application exception. Logs can then retain the causal chain while HTTP/CLI/queue boundaries expose only safe stable semantics.

PHP error levels include warnings, notices, deprecations, and fatal categories. error_reporting() selects what the built-in handler reports. set_error_handler() installs a callback for specified levels that are handleable at runtime; returning false delegates to PHP’s standard handler.

A custom handler can throw ErrorException, unifying selected diagnostics with exception control flow. That policy must be deliberate. Converting deprecations to exceptions in production can turn upgrade information into outages; ignoring warnings can let corrupted assumptions proceed. Many teams make tests/CI strict, report production diagnostics with context, and define a separate deprecation budget.

Some failures cannot be intercepted by a user error handler in the failing execution context, including particular startup, parse/compile, and core conditions. Engine/version/SAPI details matter. Do not claim that registering a handler converts “all PHP errors” into exceptions.

The error-suppression operator changes reporting visibility but not reality. Libraries should not globally change error reporting or rely on suppression to define control flow. If maintaining an API that emits warnings, isolate it, inspect its documented result, and attach context without leaking secrets.

set_exception_handler() receives an otherwise uncaught Throwable. It is the last reporting/rendering boundary, not a place from which the failed stack resumes. A web front controller may translate to a safe error response; a CLI process should emit diagnostic context and a non-zero exit; a worker should let its supervisor/queue integration record failure according to retry policy.

The handler itself must be small and defensive. If logging, templating, or dependency resolution fails inside it, the process has fewer fallback paths. Keep an emergency reporting path that needs minimal allocation and no remote dependency. Avoid returning stack traces, SQL, environment data, tokens, or provider payloads to users.

Catch-all blocks deep in application code are different: catch (Throwable) followed by logging and continuation can convert programming failures into plausible corrupt results. Catch broadly only at isolation boundaries or when rethrowing after cleanup/context.

A finally block runs when control leaves its try through normal return or a thrown exception, subject to process-level termination limits. It is the right local structure for releasing locks, restoring temporary state, closing an explicitly owned resource, or recording duration.

Do not let cleanup mask the original failure. An exception thrown from finally can replace what callers observe, though causal information may be retained depending on the sequence. Cleanup should be idempotent where practical and separately observable. Database transaction helpers normally roll back on thrown failures, but they cannot undo an HTTP call, published message, email, or filesystem/object-store effect.

An ordinary domain alternative—declined payment, unavailable slot, validation result—may be better represented as a result or named outcome than an exception if callers routinely branch on it. Exceptions fit failures that interrupt the requested path and need propagation. The choice should preserve information, not force every non-success into null, false, or a generic exception.

Shutdown functions are last-chance observation

Section titled “Shutdown functions are last-chance observation”

Registered shutdown functions run as the request or process is ending, including after many fatal terminations. error_get_last() can reveal the last traditional error. This is useful for minimal fatal-error telemetry and resource cleanup that is best effort.

It is not general recovery. The process may be at its memory limit, past its maximum execution time, partially initialized, holding inconsistent in-memory state, or being terminated in a way that does not run PHP shutdown code. There may be too little memory to build a rich log record. A shutdown callback cannot roll back an already committed database transaction or external effect and should not attempt to continue the business operation.

Reserve memory can sometimes improve the chance of recording memory exhaustion, but supervisors, web servers, container/runtime signals, and centralized logs remain part of observability. Forced termination such as SIGKILL, host loss, or process crash can bypass language cleanup entirely.

Destructors are not transaction boundaries

Section titled “Destructors are not transaction boundaries”

__destruct() runs when an object is destroyed, which may be when its reference count reaches zero or during shutdown. Cycles, garbage collection, shutdown order, exceptions, and abrupt termination make destructor timing unsuitable for correctness-critical effects.

Destructors can support best-effort local resource wrappers, but explicit close()/commit()/release() methods combined with try/finally make ownership and failure visible. Never depend on a destructor to publish an event, charge/refund money, acknowledge queue work, or release a distributed lock before its lease expires.

At each boundary, ask:

  1. Is this an expected domain outcome, a transient dependency failure, bad input, a programmer error, or process/resource failure?
  2. What state or external effect may already have changed?
  3. Is retry safe, and where is idempotency established?
  4. What context can be logged safely, with which correlation and causal chain?
  5. Who converts the failure into HTTP, CLI, queue, or monitoring semantics?

A retryable transport exception does not prove the operation failed; a timeout may mean the provider succeeded but its response was lost. Conversely, an engine Error should not normally be retried indefinitely. Classification belongs near the boundary with enough knowledge to distinguish unknown outcome from safe failure.

  • A global handler throws deprecations in production after an upgrade and turns warnings into 500 responses.
  • A catch-all logs and returns success, causing a queue message to be acknowledged despite incomplete work.
  • A shutdown logger allocates a large object after memory exhaustion and records nothing.
  • A destructor attempts remote cleanup but the worker is forcibly terminated.
  • Exception translation drops the previous throwable, erasing the provider or database cause.
  • An HTTP response exposes the stack or raw vendor body while logging omits the correlation ID needed for diagnosis.
  • Current: PHP 8.5 represents many engine failures with Error subclasses and includes backtraces for more fatal-error situations. New handler-inspection functions can help infrastructure compose safely.
  • Common: Frameworks centralize error-to-exception conversion, reporting, and rendering. Know the framework policy before adding another global handler.
  • Legacy: Suppression, global set_error_handler() conversion, sentinel returns, and shutdown-based “recovery” require characterization. Preserve operational evidence while moving responsibility to explicit boundaries.