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.
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
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;
}
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
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;
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.