Phase 3 · Object-Oriented & Modern PHPModule 18~44 min read

Exceptions & Reliable Error Handling

Design explicit failure paths with Throwable, custom exceptions, cleanup, global handlers, and secure logging.

What you'll learn

Turn failures into explicit control flow without leaking internals. You will create meaningful exception types, preserve causes, guarantee cleanup, and centralize logging and user-safe responses.

By the end of this lesson, you'll be able to:

  • Throw domain-specific exceptions
  • Catch only where recovery or translation is possible
  • Log diagnostic context while returning safe messages

Core mental model

Professional PHP is less about memorizing APIs and more about choosing a clear boundary for each responsibility. Use this table as a decision guide while reading the examples.

ConceptWhat it protectsDecision rule
ExceptionExceptional failure pathThrow when the contract cannot return a valid result.
TranslationStable abstractionWrap infrastructure errors at the layer boundary.
finallyGuaranteed cleanupRelease resources whether work succeeds or fails.

Professional workflow

Build the feature in small, verifiable steps. Each step leaves the system in a state you can test.

  1. Describe the failure boundary boundary: its inputs, outputs, invariants, and expected failures.
  2. Implement the smallest happy path behind an explicit contract.
  3. Add validation and translate low-level failures into language the caller understands.
  4. Exercise the boundary with realistic data, then inspect output, logs, and resource cleanup.
  5. Refactor only after behavior is protected by a repeatable check.

Make the boundary visible

Name inputs, outputs, side effects, and failure cases before adding framework or infrastructure code. That habit keeps advanced PHP understandable as the application grows.

Guided code lab

Name a domain failure

The service exposes a failure the caller can understand and preserves the original database cause.

EnrollmentService.php
<?php
final class CourseIsFull extends RuntimeException {}

final readonly class EnrollmentService
{
    public function __construct(private PDO $db) {}
    public function enroll(int $courseId, int $studentId): void
    {
        try {
            // check capacity and insert...
            throw new PDOException('duplicate');
        } catch (PDOException $e) {
            throw new RuntimeException('Could not save enrollment', previous: $e);
        }
    }
}

Handle errors at the application edge

One handler logs full context and sends a generic production response.

public/index.php
<?php
set_exception_handler(function (Throwable $error): void {
    error_log(json_encode([
        'type' => $error::class,
        'message' => $error->getMessage(),
        'trace_id' => $_SERVER['HTTP_X_REQUEST_ID'] ?? null,
    ], JSON_THROW_ON_ERROR));

    http_response_code(500);
    header('Content-Type: application/json');
    echo json_encode(['error' => 'Unexpected server error']);
});

Production practice

Contract

Document exceptions that callers are expected to recover from; do not use them for ordinary branching.

Verification

Test the exception type, safe public message, preserved previous cause, and cleanup behavior.

Operations

Attach request IDs and structured context, but never log passwords, tokens, or full personal data.

Common failure mode

Catching Throwable everywhere and returning null destroys the failure signal. The program continues with invalid assumptions and no diagnosis.

Independent workshop

Add a failure strategy to a file importer that validates rows, continues recoverable errors, and aborts infrastructure failures.

Your finished workshop must include:

  • Custom validation and import exceptions
  • A finally block for file cleanup
  • Safe CLI output plus a diagnostic log

Definition of done

Run the happy path and at least two failure paths, explain one design tradeoff in a short README, and leave the code formatted and ready for review.

Recap & quick check

Key takeaways

  • Throw where a contract fails.
  • Catch where you can recover or translate.
  • Exception chaining preserves the root cause.
  • The application edge owns the public error response.

Quick check

1. Where should an infrastructure exception become a domain-friendly one?

2. What always runs after try/catch?

3. Should a production response include a stack trace?

Keep the workshop: later phases deliberately build on these boundaries, so today's small example can become part of your portfolio architecture.