Error Handling Advanced
By the end of this lesson you'll catch failures gracefully with try / catch / finally , design your own exception classes, chain causes for clean logs, and report errors safely in production — so a single bad request never takes your whole app down.
Part of the free Php course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
1️⃣ try / catch / finally
When code might fail, you wrap it in a try block. If anything inside throws an exception, PHP jumps straight to a matching catch block instead of crashing. An exception is simply an object that means "stop — something went wrong" and carries a message describing what. The optional finally block runs afterwards no matter what — success or failure — which makes it the perfect place for cleanup like closing a file or a database connection.
Read the output order carefully: for the failing case, the catch runs and then the finally still runs. That guarantee is why you put cleanup in finally rather than copying it into both the success and failure paths.
2️⃣ Errors vs Exceptions, and Catching Specific Types
PHP has two families of "throwable" things. An Exception is a recoverable problem your code raises on purpose with throw — bad input, a missing record. An Error (like TypeError or DivisionByZeroError ) is an engine-level fault, usually a bug. Both implement the Throwable interface, so catch (Throwable $e) catches either. The key habit: list your most specific catch first , because PHP uses the first one that matches.
Notice you can even catch an engine Error such as DivisionByZeroError . You generally shouldn't rely on that to paper over bugs, but it's invaluable as a last-resort net so one unexpected fault doesn't take down the whole request.
3️⃣ Custom Exception Classes
Throwing the generic Exception everywhere forces every caller to catch everything and then inspect the message string to figure out what happened. Instead, make your own types by writing class ValidationException extends Exception . Now you can catch (ValidationException $e) separately from a not-found error, and each class can carry extra data — here an HTTP status code — so the error knows how it should be handled.
4️⃣ Exception Chaining
Often a low-level failure (a file won't open) should become a clearer high-level one (the config can't load) — but you don't want to lose the original cause. Exception chaining solves this: when you re-throw, pass the original exception as the third constructor argument. Later, getPrevious() walks back to the root cause, so users see the friendly message while your logs keep the full trail.
5️⃣ Global Handlers, Logging & Dev vs Production
No matter how careful you are, something will eventually slip past every try . set_exception_handler() registers a last line of defence that runs for any uncaught exception, and set_error_handler() lets you turn old-style warnings into exceptions so they flow through the same path. The other half is where errors go : in production set display_errors to 0 so visitors never see raw errors (they leak paths and secrets), and send the details to a log with error_log() instead.
The golden rule sits in those two lines: log the full detail, show the user a friendly generic message. On your own machine you'd flip $isProduction to false and turn display_errors back on so you see everything immediately.
🎯 Your Turn
Now you try. The script below is almost complete — fill in each ___ using the 👉 hint, then run it and check it against the Output panel.
One more — this time you'll define a custom exception type and throw it. Fill in the extends keyword and the class name.
Common Errors (and the fix)
- Swallowing exceptions silently — an empty catch (Exception $e) { } hides the failure and leaves you debugging a "nothing happened" mystery for hours. At an absolute minimum, log it: error_log($e->getMessage()); . Catch an exception only when you can actually do something about it.
- Exposing errors in production — leaving display_errors on shows visitors file paths, SQL, and stack traces, which is both ugly and a security leak. Set ini_set("display_errors", "0") in production, keep error_reporting(E_ALL) , and route detail to error_log() .
- Catching too broadly — catch (Throwable $e) as your only catch hides bugs you'd want to fix and treats a typo the same as a missing user. Catch the specific types first , then add a broad catch as a deliberate safety net, not a crutch.
- "Uncaught DivisionByZeroError" (or TypeError ) — these are Error s, not Exception s, so catch (Exception $e) won't catch them. Use catch (Throwable $e) (or the specific DivisionByZeroError ) when you need to handle engine-level faults.
- Not logging at all — handling an exception but never recording it means recurring problems stay invisible. Always write something to error_log() (or a logger) so monitoring tools can surface patterns.
Pro Tips
- 💡 Catch only what you can handle. If a function can't sensibly recover from an exception, let it bubble up to a caller (or the global handler) that can.
- 💡 PHP 8 lets you catch multiple types at once: catch (TypeError | ValueError $e) shares one block between related errors.
- 💡 Use match in your global handler to map exception classes to HTTP status codes cleanly instead of a long if/elseif chain.
📋 Quick Reference — Error Handling
Syntax
Example
What It Does
try / catch
try { ... } catch (Exception $e) { ... }
Run risky code; handle a thrown exception
finally
finally { ... }
Always runs — cleanup after try/catch
throw
throw new RuntimeException("bad");
Raise an exception on purpose
extends Exception
class MyEx extends Exception { }
Define a custom exception type
getPrevious()
$e->getPrevious()
Get the chained root cause
set_exception_handler()
set_exception_handler($fn)
Handle any uncaught exception globally
set_error_handler()
set_error_handler($fn)
Turn PHP warnings into exceptions
error_log()
error_log("oops")
Write a message to the server log
display_errors
ini_set("display_errors","0")
Show errors (dev) or hide them (prod)
Frequently Asked Questions
Mini-Challenge: A Safe Endpoint
No code is filled in this time — just a brief and an outline. Write it yourself, run it on onecompiler.com/php or your own machine, then check your result against the expected output in the comments. This is the same throw → catch → cleanup loop you'll use in every real request handler.
🎉 Lesson Complete!
- ✅ An Exception is a recoverable problem you throw; an Error is an engine fault — both are Throwable
- ✅ try guards risky code, catch handles a thrown exception, and finally always runs
- ✅ Catch the most specific type first ; use throw to raise your own
- ✅ Custom exceptions are just classes that extends Exception and can carry extra data
- ✅ Chain a cause as the 3rd argument and recover it with getPrevious()
- ✅ Register global handlers, error_log() the details, and hide them from users with display_errors off in production
- ✅ Next lesson: Working with REST APIs — fetch and send data with cURL, streams, and Guzzle
Practice quiz
What interface do both Error and Exception implement?
- Catchable
- Stringable
- Throwable
- Iterator
Answer: Throwable. Everything you can throw implements Throwable, so catch (Throwable $e) is a true catch-all.
What is the difference between an Error and an Exception?
- An Exception is a recoverable problem you throw; an Error is an engine-level fault
- An Error is recoverable; an Exception is fatal
- They are identical
- Errors are thrown only in functions
Answer: An Exception is a recoverable problem you throw; an Error is an engine-level fault. You throw Exceptions on purpose for recoverable problems; the engine throws Errors (TypeError, etc.) for serious faults.
Will catch (Exception $e) catch a TypeError?
- Yes — all throwables are Exceptions
- Only in strict mode
- Only if you rethrow it
- No — a TypeError is an Error, not an Exception
Answer: No — a TypeError is an Error, not an Exception. TypeError is an Error, not an Exception. Use catch (Throwable $e) or the specific type to catch it.
Does the finally block always run, even after a return or throw?
- No — only when the try succeeds
- Yes — it always runs (except on exit/fatal before reaching it)
- Only when an exception is thrown
- Only when there is no catch
Answer: Yes — it always runs (except on exit/fatal before reaching it). finally always runs after try, which is why it is the right place for cleanup like closing files.
How do you order catch blocks for different exception types?
- Most specific type first, broader ones after
- Broadest type first
- Order does not matter
- Only one catch is allowed
Answer: Most specific type first, broader ones after. PHP uses the first matching catch, so list specific types first and a broad Throwable as a safety net.
How do you define a custom exception type?
- class MyException implements Exception {}
- trait MyException uses Exception {}
- class MyException extends Exception {}
- function MyException() {}
Answer: class MyException extends Exception {}. A custom exception is just a class that extends Exception; it can carry extra data too.
How do you attach the original cause when re-throwing an exception?
- Pass it as the first argument
- Pass it as the third constructor argument
- Call setCause() on it
- You cannot chain exceptions
Answer: Pass it as the third constructor argument. throw new ConfigException("...", 0, $cause) chains the cause; getPrevious() walks back to it.
Which method retrieves the chained root cause of an exception?
- getCause()
- getRoot()
- getParent()
- getPrevious()
Answer: getPrevious(). $e->getPrevious() returns the previously-attached exception that was wrapped.
In production, how should you handle errors safely?
- Show full stack traces to users
- Set display_errors to 0 and log details with error_log()
- Disable error_reporting entirely
- Echo $e->getMessage() to the page
Answer: Set display_errors to 0 and log details with error_log(). Hide raw errors from visitors (they leak paths and secrets), but keep reporting and route detail to logs.
What does set_exception_handler() register?
- A handler that runs before every try block
- A replacement for catch blocks
- A last-resort handler for any uncaught exception
- A handler for syntax errors
Answer: A last-resort handler for any uncaught exception. It runs for any exception nothing else caught, just before the script would die — your last line of defence.
Continue this course
- Previous: Dependency Injection
- Next: Working with REST APIs