05: Error Handling - Try/Catch & Type Safety
Error Handling: Try/Catch & Type Safety
Section titled “Error Handling: Try/Catch & Type Safety”Overview
Section titled “Overview”Both TypeScript and PHP use try/catch for exception handling, but PHP has a richer exception hierarchy and more powerful error handling capabilities. This chapter shows you how to write robust, type-safe error handling in PHP.
Learning Objectives
Section titled “Learning Objectives”By the end of this chapter, you’ll be able to:
- ✅ Use try/catch/finally blocks in PHP
- ✅ Understand PHP’s Error and Exception hierarchy
- ✅ Create custom exception classes
- ✅ Handle multiple exception types
- ✅ Use type-safe error handling patterns
- ✅ Implement Result types for functional error handling
- ✅ Apply error handling best practices
Code Examples
Section titled “Code Examples”📁 View Code Examples on GitHub
This chapter includes error handling examples:
- Custom exception classes
- Exception chaining
- Result type pattern
- Validation examples
Run the examples:
cd code/php-typescript-developers/chapter-05php custom-exceptions.phpphp result-type.phpBasic Try/Catch
Section titled “Basic Try/Catch”TypeScript
Section titled “TypeScript”try { throw new Error("Something went wrong");} catch (error) { if (error instanceof Error) { console.error(error.message); }} finally { console.log("Cleanup code");}<?phpdeclare(strict_types=1);
try { throw new Exception("Something went wrong");} catch (Exception $e) { echo "Error: " . $e->getMessage() . PHP_EOL;} finally { echo "Cleanup code" . PHP_EOL;}Key Similarities:
- ✅
try,catch,finallywork the same - ✅
finallyalways executes - ✅ Exceptions bubble up if not caught
Key Differences:
- PHP requires type hint for caught exception:
catch (Exception $e) - PHP has explicit exception variable in catch block
Exception Hierarchy
Section titled “Exception Hierarchy”TypeScript (Simple)
Section titled “TypeScript (Simple)”// Basic Error classclass CustomError extends Error { constructor(message: string) { super(message); this.name = "CustomError"; }}
try { throw new CustomError("Custom error");} catch (error) { if (error instanceof CustomError) { console.error("Custom:", error.message); } else if (error instanceof Error) { console.error("Generic:", error.message); }}PHP (Rich Hierarchy)
Section titled “PHP (Rich Hierarchy)”<?phpdeclare(strict_types=1);
// PHP has two base classes:// 1. Exception - for recoverable errors// 2. Error - for serious errors (usually fatal)
// Custom exceptionclass CustomException extends Exception {}
try { throw new CustomException("Custom error");} catch (CustomException $e) { echo "Custom: " . $e->getMessage() . PHP_EOL;} catch (Exception $e) { echo "Generic: " . $e->getMessage() . PHP_EOL;}PHP Exception Hierarchy:
Throwable (interface)├── Exception│ ├── LogicException│ │ ├── BadFunctionCallException│ │ ├── BadMethodCallException│ │ ├── DomainException│ │ ├── InvalidArgumentException│ │ ├── LengthException│ │ └── OutOfRangeException│ ├── RuntimeException│ │ ├── OutOfBoundsException│ │ ├── OverflowException│ │ ├── RangeException│ │ ├── UnderflowException│ │ └── UnexpectedValueException│ └── Other built-in exceptions...└── Error ├── TypeError ├── ParseError ├── ArithmeticError │ └── DivisionByZeroError ├── AssertionError └── CompileErrorException vs Error
Section titled “Exception vs Error”Exception (Recoverable)
Section titled “Exception (Recoverable)”<?phpdeclare(strict_types=1);
class UserNotFoundException extends Exception {}
function findUser(int $id): array { // Simulated database lookup if ($id !== 1) { throw new UserNotFoundException("User {$id} not found"); } return ['id' => $id, 'name' => 'Alice'];}
try { $user = findUser(999);} catch (UserNotFoundException $e) { // Recoverable: show error to user, return default, etc. echo "Error: " . $e->getMessage() . PHP_EOL; $user = ['id' => 0, 'name' => 'Guest'];}Error (Serious Issues)
Section titled “Error (Serious Issues)”<?phpdeclare(strict_types=1);
function processData(string $data): void { // TypeError is an Error, not Exception $length = strlen($data);}
try { processData(123); // Wrong type} catch (TypeError $e) { // Serious issue: indicates a programming error echo "Type Error: " . $e->getMessage() . PHP_EOL;}When to Use:
- Exception: Business logic errors (user not found, validation failed)
- Error: Programming errors (type errors, parse errors, fatal issues)
Multiple Catch Blocks
Section titled “Multiple Catch Blocks”TypeScript
Section titled “TypeScript”class ValidationError extends Error {}class NetworkError extends Error {}
try { // Some operation throw new ValidationError("Invalid input");} catch (error) { if (error instanceof ValidationError) { console.error("Validation:", error.message); } else if (error instanceof NetworkError) { console.error("Network:", error.message); } else { console.error("Unknown:", error); }}<?phpdeclare(strict_types=1);
class ValidationException extends Exception {}class NetworkException extends Exception {}
try { // Some operation throw new ValidationException("Invalid input");} catch (ValidationException $e) { echo "Validation: " . $e->getMessage() . PHP_EOL;} catch (NetworkException $e) { echo "Network: " . $e->getMessage() . PHP_EOL;} catch (Exception $e) { echo "Unknown: " . $e->getMessage() . PHP_EOL;}Union Catch (PHP 8.0+)
Section titled “Union Catch (PHP 8.0+)”<?phpdeclare(strict_types=1);
class ValidationException extends Exception {}class AuthenticationException extends Exception {}
try { throw new ValidationException("Invalid input");} catch (ValidationException | AuthenticationException $e) { // Handle both exception types the same way echo "Client error: " . $e->getMessage() . PHP_EOL;}Custom Exception Classes
Section titled “Custom Exception Classes”TypeScript
Section titled “TypeScript”class HttpException extends Error { constructor( public statusCode: number, message: string ) { super(message); this.name = "HttpException"; }}
class NotFoundException extends HttpException { constructor(message: string) { super(404, message); }}
try { throw new NotFoundException("Resource not found");} catch (error) { if (error instanceof HttpException) { console.error(`HTTP ${error.statusCode}: ${error.message}`); }}<?phpdeclare(strict_types=1);
class HttpException extends Exception { public function __construct( private int $statusCode, string $message = "", int $code = 0, ?Throwable $previous = null ) { parent::__construct($message, $code, $previous); }
public function getStatusCode(): int { return $this->statusCode; }}
class NotFoundException extends HttpException { public function __construct(string $message = "Resource not found") { parent::__construct(404, $message); }}
try { throw new NotFoundException("User not found");} catch (HttpException $e) { echo "HTTP {$e->getStatusCode()}: {$e->getMessage()}" . PHP_EOL;}Exception Context and Stack Traces
Section titled “Exception Context and Stack Traces”PHP Exception Methods
Section titled “PHP Exception Methods”<?phpdeclare(strict_types=1);
try { throw new Exception("Something went wrong", 500);} catch (Exception $e) { echo "Message: " . $e->getMessage() . PHP_EOL; echo "Code: " . $e->getCode() . PHP_EOL; echo "File: " . $e->getFile() . PHP_EOL; echo "Line: " . $e->getLine() . PHP_EOL; echo "Trace: " . $e->getTraceAsString() . PHP_EOL;}Available Methods:
getMessage(): Error messagegetCode(): Numeric error codegetFile(): File where exception was throwngetLine(): Line numbergetTrace(): Stack trace as arraygetTraceAsString(): Stack trace as stringgetPrevious(): Previous exception (for chaining)
Exception Chaining
Section titled “Exception Chaining”TypeScript
Section titled “TypeScript”class DatabaseError extends Error {}class QueryError extends Error {}
try { try { throw new QueryError("Invalid SQL"); } catch (error) { throw new DatabaseError("Database operation failed", { cause: error }); }} catch (error) { if (error instanceof DatabaseError && error.cause) { console.error("Database:", error.message); console.error("Cause:", (error.cause as Error).message); }}<?phpdeclare(strict_types=1);
class DatabaseException extends Exception {}class QueryException extends Exception {}
try { try { throw new QueryException("Invalid SQL"); } catch (QueryException $e) { // Chain exceptions with $previous parameter throw new DatabaseException("Database operation failed", 0, $e); }} catch (DatabaseException $e) { echo "Database: " . $e->getMessage() . PHP_EOL;
$previous = $e->getPrevious(); if ($previous) { echo "Cause: " . $previous->getMessage() . PHP_EOL; }}Result Type Pattern (Functional Error Handling)
Section titled “Result Type Pattern (Functional Error Handling)”For type-safe error handling without exceptions:
TypeScript
Section titled “TypeScript”type Result<T, E = Error> = | { success: true; value: T } | { success: false; error: E };
function divide(a: number, b: number): Result<number> { if (b === 0) { return { success: false, error: new Error("Division by zero") }; } return { success: true, value: a / b };}
const result = divide(10, 2);if (result.success) { console.log("Result:", result.value);} else { console.error("Error:", result.error.message);}<?phpdeclare(strict_types=1);
/** * @template T * @template E of Exception */class Result { private function __construct( private bool $success, private mixed $value, private ?Throwable $error ) {}
/** * @template V * @param V $value * @return Result<V, never> */ public static function ok(mixed $value): self { return new self(true, $value, null); }
/** * @template Err of Throwable * @param Err $error * @return Result<never, Err> */ public static function err(Throwable $error): self { return new self(false, null, $error); }
public function isOk(): bool { return $this->success; }
public function isErr(): bool { return !$this->success; }
/** * @return T */ public function unwrap(): mixed { if ($this->success) { return $this->value; } throw new RuntimeException("Called unwrap on error result"); }
/** * @return E */ public function unwrapErr(): Throwable { if (!$this->success) { return $this->error; } throw new RuntimeException("Called unwrapErr on ok result"); }}
/** * @return Result<float, Exception> */function divide(float $a, float $b): Result { if ($b === 0) { return Result::err(new Exception("Division by zero")); } return Result::ok($a / $b);}
$result = divide(10, 2);if ($result->isOk()) { echo "Result: " . $result->unwrap() . PHP_EOL;} else { echo "Error: " . $result->unwrapErr()->getMessage() . PHP_EOL;}Best Practices
Section titled “Best Practices”1. Use Specific Exception Types
Section titled “1. Use Specific Exception Types”❌ Bad:
<?phpthrow new Exception("User not found");throw new Exception("Invalid email");throw new Exception("Network error");✅ Good:
<?phpthrow new UserNotFoundException("User not found");throw new ValidationException("Invalid email");throw new NetworkException("Network error");2. Don’t Catch Everything
Section titled “2. Don’t Catch Everything”❌ Bad:
<?phptry { // Some code} catch (Throwable $e) { // Silently ignore - BAD!}✅ Good:
<?phptry { // Some code} catch (ValidationException $e) { // Handle specific error return response(['error' => $e->getMessage()], 400);} catch (NotFoundException $e) { return response(['error' => $e->getMessage()], 404);}// Let other exceptions bubble up3. Provide Context
Section titled “3. Provide Context”❌ Bad:
<?phpthrow new Exception("Error");✅ Good:
<?phpthrow new ValidationException( "Invalid email format: '{$email}'. Expected format: user@domain.com");4. Use Finally for Cleanup
Section titled “4. Use Finally for Cleanup”<?php$file = fopen('data.txt', 'r');try { // Process file $content = fread($file, filesize('data.txt'));} finally { // Always close file, even if exception occurs fclose($file);}Practical Example: API Request Handler
Section titled “Practical Example: API Request Handler”TypeScript
Section titled “TypeScript”class ApiError extends Error { constructor( public statusCode: number, message: string ) { super(message); }}
async function fetchUser(id: number): Promise<User> { try { const response = await fetch(`/api/users/${id}`);
if (!response.ok) { throw new ApiError( response.status, `Failed to fetch user: ${response.statusText}` ); }
return await response.json(); } catch (error) { if (error instanceof ApiError) { // Handle API errors console.error(`API Error ${error.statusCode}: ${error.message}`); throw error; } else { // Handle network errors console.error("Network error:", error); throw new ApiError(500, "Network request failed"); } }}<?phpdeclare(strict_types=1);
class ApiException extends Exception { public function __construct( private int $statusCode, string $message, ?Throwable $previous = null ) { parent::__construct($message, $statusCode, $previous); }
public function getStatusCode(): int { return $this->statusCode; }}
class NetworkException extends Exception {}
function fetchUser(int $id): array { try { $ch = curl_init("https://api.example.com/users/{$id}"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new NetworkException(curl_error($ch)); }
curl_close($ch);
if ($httpCode !== 200) { throw new ApiException( $httpCode, "Failed to fetch user: HTTP {$httpCode}" ); }
return json_decode($response, true); } catch (ApiException $e) { // Handle API errors error_log("API Error {$e->getStatusCode()}: {$e->getMessage()}"); throw $e; } catch (NetworkException $e) { // Handle network errors error_log("Network error: {$e->getMessage()}"); throw new ApiException(500, "Network request failed", $e); }}Hands-On Exercise
Section titled “Hands-On Exercise”Task 1: Create a Validation System
Section titled “Task 1: Create a Validation System”Build a validation system with custom exceptions:
Requirements:
ValidationExceptionbase classInvalidEmailException,InvalidAgeExceptionsubclassesValidatorclass with methods to validate email and age- Throw appropriate exceptions with helpful messages
Solution
<?phpdeclare(strict_types=1);
class ValidationException extends Exception {}class InvalidEmailException extends ValidationException {}class InvalidAgeException extends ValidationException {}
class Validator { public function validateEmail(string $email): void { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new InvalidEmailException( "Invalid email format: '{$email}'" ); } }
public function validateAge(int $age): void { if ($age < 0 || $age > 150) { throw new InvalidAgeException( "Age must be between 0 and 150, got: {$age}" ); } }
public function validateUser(array $data): array { $errors = [];
try { $this->validateEmail($data['email'] ?? ''); } catch (InvalidEmailException $e) { $errors['email'] = $e->getMessage(); }
try { $this->validateAge($data['age'] ?? 0); } catch (InvalidAgeException $e) { $errors['age'] = $e->getMessage(); }
if (!empty($errors)) { throw new ValidationException(json_encode($errors)); }
return $data; }}
// Test$validator = new Validator();
try { $validator->validateUser([ 'email' => 'invalid-email', 'age' => 200 ]);} catch (ValidationException $e) { echo "Validation errors: " . $e->getMessage() . PHP_EOL;}Task 2: Implement Retry Logic
Section titled “Task 2: Implement Retry Logic”Create a retry function that catches exceptions and retries:
Solution
<?phpdeclare(strict_types=1);
class RetryException extends Exception {}
/** * @template T * @param callable(): T $operation * @return T */function retry(callable $operation, int $maxAttempts = 3, int $delayMs = 100): mixed { $lastException = null;
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) { try { return $operation(); } catch (Exception $e) { $lastException = $e; echo "Attempt {$attempt} failed: {$e->getMessage()}" . PHP_EOL;
if ($attempt < $maxAttempts) { usleep($delayMs * 1000); } } }
throw new RetryException( "Operation failed after {$maxAttempts} attempts", 0, $lastException );}
// Test$attempts = 0;try { $result = retry(function() use (&$attempts) { $attempts++; if ($attempts < 3) { throw new Exception("Temporary failure"); } return "Success!"; });
echo "Result: {$result}" . PHP_EOL;} catch (RetryException $e) { echo "All retries failed: " . $e->getMessage() . PHP_EOL;}Key Takeaways
Section titled “Key Takeaways”- Try/catch/finally syntax is nearly identical between TS and PHP
- PHP has two base types:
Exception(recoverable) andError(serious) - Type hints required in catch blocks:
catch (Exception $e)- cannot catch without type - Rich exception hierarchy in PHP with specific exception types (InvalidArgumentException, RuntimeException, etc.)
- Union catch (PHP 8.0+) handles multiple exception types:
catch (A | B $e) - Exception chaining via
$previousparameter preserves context across layers - Result type pattern provides functional error handling alternative to exceptions
- Custom exceptions should extend appropriate base class and add domain-specific context
catch (Throwable $e)catches everything (both Exception and Error)- Set exception/error handlers globally with
set_exception_handler()andset_error_handler() - Never catch
Errorin application code - indicates serious problems - Always include exception message when rethrowing for debugging context
Comparison Table
Section titled “Comparison Table”| Feature | TypeScript | PHP |
|---|---|---|
| Try/Catch | ✅ | ✅ |
| Finally | ✅ | ✅ |
| Type Hint in Catch | Optional | Required |
| Exception Hierarchy | Simple | Rich (Exception/Error) |
| Multiple Catch | Manual checks | Native |
| Union Catch | ❌ | ✅ (PHP 8.0+) |
| Exception Chaining | cause option | $previous param |
| Stack Traces | stack property | getTrace() method |
Next Steps
Section titled “Next Steps”Congratulations! You’ve completed Phase 1: Foundations of the series. You now understand PHP’s type system, syntax, functions, OOP, and error handling.
Next Chapter: 06: Package Management: npm vs Composer
Phase 2 Preview: Ecosystem (Package management, testing, code quality, build tools, debugging)
Resources
Section titled “Resources”Questions or feedback? Open an issue on GitHub