Tutorials Logic, IN info@tutorialslogic.com

PHP Error Handling: Exceptions, Logging, and Safe Responses

PHP Failure Model

PHP failures appear through configured error reporting and the Throwable hierarchy. Exceptions and Errors unwind until a matching catch or final handler, while finally provides cleanup but can also replace earlier outcomes if misused.

Production handling separates safe responses from diagnostic logs, preserves cause chains, makes transactions and cleanup explicit, and tests failure impact instead of merely catching every Throwable.

Throw and Catch

Reject an Invalid Percentage

Reject an Invalid Percentage
<?php
function applyDiscount(float $total, float $percent): float
{
    if ($percent < 0 || $percent > 100) {
        throw new InvalidArgumentException('Percent must be between 0 and 100.');
    }

    return $total * (1 - $percent / 100);
}

try {
    echo applyDiscount(500, 20);
} catch (InvalidArgumentException $exception) {
    echo $exception->getMessage();
}
Output
400

Exception Types

Catch a specific exception when the response differs by failure type. A final Throwable catch at an application boundary can log unexpected failures and return a generic response, but domain code should not convert every defect into success.

Failure Possible type
Invalid function argument InvalidArgumentException
Runtime resource failure RuntimeException
JSON encoding or decoding failure JsonException
PDO operation failure PDOException
Type contract violation TypeError

Cleanup with finally

finally runs whether the try block succeeds or throws. Use it to close resources or release application-level state that must always be cleaned up.

Always Close a Stream

Always Close a Stream
<?php
$handle = fopen(__FILE__, 'rb');
if ($handle === false) {
    throw new RuntimeException('Could not open file.');
}

try {
    echo fgets($handle);
} finally {
    fclose($handle);
}

Development and Production

Environment User response Diagnostic detail
Development Detailed local error page Display or local debugger plus logs
Production Generic safe message and status Protected logs and monitoring
  • Use error_reporting(E_ALL) during development.
  • Do not display stack traces, SQL, credentials, or filesystem paths to public users.
  • Log a request or correlation identifier so a user report can be matched to diagnostics.
  • Avoid the @ suppression operator; handle the documented failure result.

Error Reporting Policy

PHP reports notices, warnings, deprecations, and errors under the configured `error_reporting` mask. Development should surface all relevant issues, including deprecations before an upgrade. Production should log actionable detail while returning a generic response that does not expose paths, SQL, configuration, or stack traces.

`display_errors` controls direct output and should be disabled in production; `log_errors` controls logging. These directives are not substitutes for an application exception boundary. Configuration can vary by SAPI and loaded ini files, so verify the effective settings in web and CLI environments separately.

The `@` error-control operator changes reporting for one expression and hides evidence from normal output. Avoid it in application code. Prefer an API that exposes failure through return values or Throwable, then handle that contract explicitly.

Deprecations are advance notice of incompatible future behavior. Track them by code owner and runtime target instead of suppressing the category globally. A clean test suite under the next supported PHP version is part of upgrade readiness.

  • Show detailed errors only in protected development environments.
  • Log production failures without exposing internals to users.
  • Verify effective web and CLI configuration separately.
  • Treat deprecations as scheduled migration work.

Throwable Boundaries

Both Exception and Error implement Throwable. Exceptions commonly represent application or library failures, while Error covers engine-level problems such as type and argument errors. Catch the narrowest type a boundary can meaningfully handle; a broad Throwable catch belongs at a final request or job boundary for reporting and cleanup.

Thrown objects move up the call stack until a matching catch is found, executing relevant finally blocks while unwinding. If no handler matches, the request or process terminates under host behavior. Do not depend on a global handler to continue an operation whose state may be partial.

A catch block may list multiple types when they have the same response. Order catches from specific to broad. Since PHP 8, a catch variable can be omitted when the object is genuinely unused, but preserving it is useful when context or a cause must be logged.

The throw keyword is an expression in PHP 8 and can appear in null-coalescing or arrow expressions. Use this concision only for a simple invariant. Complex validation deserves a named branch with field and operation context.

  • Catch only failures the current boundary can decide.
  • Reserve broad Throwable handling for final boundaries.
  • Order specific catches before broad catches.
  • Keep throw expressions simple and contextual.

Try, Catch, and Finally

Keep a try block around the operation whose failure shares one handling policy. A large try that covers parsing, authorization, database work, rendering, and logging makes it unclear which state was committed and can catch programming defects under an unrelated response.

A catch may recover with a valid substitute, translate to a domain exception, add context and rethrow, or produce the final protocol response. Logging and rethrowing at every layer creates duplicates. Report once at the boundary that owns the final outcome while lower layers preserve a previous exception.

Finally runs whether the try completes, returns, or throws. Use it for locks, temporary resources, transaction cleanup, and restoring process state. Avoid returning from finally because it can replace a return selected earlier, and avoid throwing from cleanup unless that replacement behavior is deliberate.

Database transactions need explicit commit and rollback ownership. Catch Throwable around the transaction unit, roll back only when active, and rethrow or translate. A finally block can release resources but should not hide whether the business operation committed.

  • Keep each try block within one failure policy.
  • Report failures once at the owning boundary.
  • Use finally for dependable cleanup, not result replacement.
  • Make transaction commit and rollback outcomes explicit.

Exception Design

Create domain-specific exceptions when callers need to distinguish outcomes such as invalid transition, conflict, not found, permission denial, or temporary dependency failure. Do not create a new class for every message if no caller handles it differently.

Exception messages support developers and operations; stable codes or types support machine decisions. Keep user-facing text separate so it can be safe, localized, and appropriate to the protocol. Never expose a raw database or filesystem exception directly in an HTTP response.

When translating an exception, pass the original Throwable as `previous` so the cause chain survives. Add operation, entity, and dependency context without repeating secrets or large payloads. Preserve the first technical cause and the final business classification.

Validation errors with many independent fields often fit a structured result better than throwing on the first field. Exceptions are strongest for abandoning the current operation, while expected alternative outcomes may deserve explicit result types.

  • Create exception types only for distinct handling decisions.
  • Separate developer diagnostics from user messages.
  • Preserve the previous Throwable during translation.
  • Use structured validation results for multiple expected issues.

Handlers and Shutdown Limits

`set_error_handler` can intercept error levels that reach the custom handler and can translate selected reportable errors to ErrorException. It cannot convert every fatal condition, and the handler should respect the active reporting mask where appropriate. Install it once during bootstrap and restore prior handlers in isolated tests.

`set_exception_handler` receives an otherwise uncaught Throwable at the global boundary. Its job is to produce the final safe response or process exit and report diagnostics. It cannot resume the failed stack frame. Keep the handler simple because dependencies may already be in a bad state.

A shutdown function can inspect `error_get_last` after termination and record certain fatal failures. Memory exhaustion may leave little capacity for logging, so reserve resources or rely on process-level logs where needed. Shutdown handling cannot make a partial request successful.

Output buffering can prevent half-rendered HTML from reaching the client when a boundary fails, but API and CLI responses need their own atomic response strategy. Do not emit diagnostic text before headers or structured error output.

  • Install handlers once at a controlled bootstrap boundary.
  • Do not expect global handlers to resume failed work.
  • Plan separately for fatal and low-memory reporting.
  • Keep final HTTP and CLI error output coherent.

Error Tests and Observability

Test each documented exception category, translated cause, cleanup path, transaction rollback, global response mapping, and logging redaction. Force dependencies to fail before, during, and after a state change. Assert persistent state as well as the thrown type.

Logs should include a correlation ID, operation, release, safe entity identifier, exception type, message, cause chain, and stack at the reporting boundary. Redact credentials, tokens, personal data, request bodies, and database connection details. Use structured fields instead of parsing formatted prose later.

Monitor rate and impact by fingerprint, endpoint, job, dependency, and release. A falling error count can hide swallowed exceptions, so pair it with success outcomes, latency, queue depth, and business completion signals.

During debugging, reproduce with the same configuration and input class, pause where the Throwable originates, and inspect the first application frame. Do not patch the final catch until the state transition that violated its contract is understood.

  • Assert rollback and cleanup with every failure.
  • Log one redacted structured event per final outcome.
  • Pair exception counts with success and completion metrics.
  • Debug the origin before modifying the final handler.
Before you move on

Mastery Check

5 checks
  • Keep detailed display disabled in production.
  • Catch the narrowest type a boundary can handle.
  • Preserve previous exceptions during translation.
  • Use finally without replacing the intended result.
  • Test rollback, cleanup, redaction, and final response mapping.

Failure Handling Check

0 of 2 checked

Q1. Where should an exception be caught?

Q2. Should production users see a stack trace?

Try this next

Trace a Failure

0 of 2 completed

  1. Reject a withdrawal larger than the balance and map it to a clear user message.
  2. Log a safe record identifier and exception message without logging secrets.
Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.