Lab 09: Error Handling & Exceptions

Objective

Use PHP's exception system: try/catch/finally, custom exception classes, exception chaining, error handlers, and PHP 8's never return type. Handle errors gracefully without crashing.

Background

PHP has two parallel error systems: the legacy trigger_error() / set_error_handler() system and the modern exception system. PHP 7+ converted most fatal errors into Error exceptions. PHP 8 added never return type (functions that always throw or exit), match exhaustiveness, and throw as an expression. Good error handling separates robust production apps from fragile scripts.

Time

30 minutes

Prerequisites

  • Lab 07 (OOP)

Tools

  • PHP 8.3 CLI

  • Docker image: zchencow/innozverse-php:latest


Lab Instructions

Step 1: Try / Catch / Finally

💡 finally always runs — even if the try block returns early or an exception is thrown. Use it to release resources: close database connections, unlock files, stop timers. If both catch and finally throw, the finally exception wins (it overwrites the caught one).

📸 Verified Output:


Step 2: Custom Exception Classes

💡 Custom exceptions carry domain contextValidationException holds an array of field errors, NotFoundException encodes the HTTP 404 code. Catching AppException catches all domain errors in one block; catching subclasses lets you handle each case specifically. Always extend from a base domain exception.

📸 Verified Output:


Step 3: Exception Chaining

💡 Exception chaining (new Exception($msg, $code, $previous)) preserves the original cause. Log systems and APM tools (like Sentry) use getPrevious() to show the full causal chain. Always chain when wrapping exceptions — never silently swallow the original error.

📸 Verified Output:


Step 4: Custom Error Handler & Throwable

💡 Throwable is the top-level interface in PHP's exception hierarchy — both Error (language errors: TypeError, ParseError, ArithmeticError) and Exception (user-thrown) implement it. catch (\Exception $e) misses Error subclasses. Use catch (\Throwable $e) for truly catch-all handlers (logging, shutdown handlers).

📸 Verified Output:


Step 5: never Return Type

💡 never return type (PHP 8.1) tells the type system "this function never returns." The type checker can then eliminate dead code warnings after calls to abort(). It's used in Laravel's abort() helper, Symfony's ThrowableInterface, and any exception-throwing utility function.

📸 Verified Output:


Step 6: Result Pattern (No Exceptions for Control Flow)

💡 The Result pattern (common in Rust, Haskell, Go) uses return values instead of exceptions for expected failures. Reserve exceptions for unexpected situations (network down, disk full). Use Result for expected failures (invalid input, not found). This makes error paths explicit and composable.

📸 Verified Output:


Step 7: Logging Errors

📸 Verified Output:


Step 8: Complete — API Error Middleware

💡 Middleware error handling is how Laravel/Symfony handle exceptions — a top-level try/catch converts exceptions to HTTP responses. ValidationHttpException → 422, HttpException → its status code, Throwable → 500 (never expose internal errors to clients). This pattern keeps controllers clean.

📸 Verified Output:


Verification

Summary

PHP exception handling is mature and expressive. You've covered try/catch/finally, custom exception hierarchies, chaining, Throwable, never, the Result pattern, structured logging, and API middleware error handling. These patterns form the backbone of robust PHP applications.

Further Reading

Last updated