Course outline

PHP / Intermediate: objects, persistence, and tests / Lesson 9

Exceptions, warnings, and failure ownership

Define which layer detects, classifies, recovers, and presents a failure. Preserve causes without swallowing programmer defects or mixing diagnostics into the response body.

About 4 min readContent reviewed
On this page

Lesson goals

  • Distinguish Throwable failures from ordinary warnings.
  • Use narrow catch and reliable finally cleanup.
  • Preserve safe context and independent error outcomes.

Not every failure is the same mechanism

PHP has exceptions and Error objects implementing Throwable, along with warnings and notices governed by error-reporting behavior. An undefined array key warning is not automatically caught by catch(Exception). TypeError belongs to the Error hierarchy, so a boundary catching Throwable can observe it, but catching broadly does not mean every programming defect should become an empty successful result. Establish expected domain failures separately from unexpected defects. A missing optional note may be normal absence; a negative price reaching an internal invariant is a different event. Classification should guide recovery rather than merely silence visible symptoms.

Add context without erasing the cause

A lower-level operation can throw a specific exception and a higher layer can add domain context using a previous exception. Keep the original cause available for private diagnostics while exposing a safe user message. Avoid logging the same stack at every layer, because duplicate reports obscure frequency and ownership. A catch that returns normally has recovered that path; if recovery is not legitimate, rethrow or translate to the documented failure type. Do not put tokens, SQL parameters with personal data, or session identifiers into exception messages that may later be rendered publicly.

php Domain context with a retained cause
<?php
declare(strict_types=1);
try {
    try { throw new RuntimeException("storage unavailable"); }
    catch (RuntimeException $cause) { throw new DomainException("course load failed", 0, $cause); }
} catch (DomainException $error) {
    echo $error->getMessage(), PHP_EOL;
    echo get_class($error->getPrevious()), PHP_EOL;
}
Expected output
course load failed
RuntimeException

The caller sees a domain failure while the previous exception remains available for controlled diagnostics.

Finally releases owned work

finally runs when leaving the protected flow in the ordinary cases described by the language, whether the operation succeeds or throws. Use it to close a stream or restore a local state flag, not to return a replacement value that hides an earlier result or error. A process crash or forced termination is not a reliable cleanup protocol, so durable consistency still needs transactions and recoverable design. The example records a controlled sequence with no real external resource. Test cleanup after success, expected failure, and an unexpected exception relevant to the operation. Keep acquisition and release ownership in the same small scope where practical.

php A visible cleanup sequence
<?php
declare(strict_types=1);
$events = [];
try {
    $events[] = "start";
    throw new InvalidArgumentException("invalid quantity");
} catch (InvalidArgumentException $error) {
    $events[] = $error->getMessage();
} finally {
    $events[] = "released";
}
echo implode(",", $events), PHP_EOL;
Expected output
start,invalid quantity,released

The known validation failure is handled and cleanup still occurs. No exception is suppressed outside the explicit contract.

Repair warnings instead of hiding them

Warnings often reveal a violated input or environment assumption. Inspect the reported location and the first value that broke the contract. The @ suppression operator can hide useful diagnostics without fixing the cause and is not a general error strategy. A controlled error handler can convert selected warnings into exceptions for a particular application or test, but its scope and restoration must be explicit. Do not assume such a handler catches every fatal condition. Keep production display settings separate from private logging, and never turn on public detailed errors as an unexplained permanent repair.

Present one coherent failure response

At an HTTP boundary, classify the failure, choose a safe status and response body, and emit once before bytes escape. A database exception is not itself a user-facing schema, and a validation error should not masquerade as HTTP success simply because JSON was generated. Preserve a correlation identifier when the application has one without exposing secrets. At a CLI boundary, use a documented exit policy and diagnostic channel. Test the original failure and a healthy control so a broken dependency does not make every negative test appear correct. Evidence should show both the intended rejection and absence of forbidden side effects.

Common errors and fixes

Every Throwable becomes an empty array

Why it happens
Unexpected defects are treated as successful absence.
How to fix it
Handle expected categories narrowly and propagate unexplained defects.
Prevent it next time
Document failure semantics and test them explicitly.

A warning is assumed to be caught by Exception

Why it happens
Error mechanisms are conflated.
How to fix it
Repair the boundary or install a scoped intentional handler.
Prevent it next time
Test the actual warning/exception class under the target runtime.

finally hides the original error

Why it happens
Cleanup returns or throws an unrelated replacement outcome.
How to fix it
Keep cleanup simple and preserve the primary failure.
Prevent it next time
Review success and failure exits together.

Check your understanding

1. Will catch(Exception $e) catch TypeError?

Show worked solution

No. TypeError is an Error implementing Throwable rather than an Exception subclass. A top boundary may catch Throwable to report unexpected failures safely, but domain recovery should remain specific. Verify the actual type and do not convert a programmer defect into a valid empty record.

2. What should be checked after a rejected reservation?

Show worked solution

Check the intended error category and that no seat, payment, or reservation row changed. A correct exception message alone cannot prove atomic behavior. Add a healthy permitted case and a controlled failure after partial work when transaction cleanup is part of the requirement.

Key takeaways

  • Failure detection, recovery, cleanup, and presentation have distinct owners.
  • Preserve causes and fix violated assumptions instead of suppressing symptoms.

Official references