advanced php language
Throwable, Exception, and Error
Throwable is the base interface for things that can be thrown and caught in PHP. Exception and Error both implement Throwable, but they usually mean different kinds of failure.
In application code, an exception often represents a failure you deliberately model or expect at a boundary: invalid input, a missing record, a failed HTTP request, or a file that cannot be read. An error often represents a programming or engine-level problem: calling a function with the wrong type, using an undefined class, or trying to call a method on a value that is not an object.
The Hierarchy
The simplified hierarchy looks like this:
Throwable
Exception
RuntimeException
InvalidArgumentException
LogicException
...
Error
TypeError
ValueError
ParseError
...
You can catch Throwable, Exception, or a more specific subclass.
<?php
declare(strict_types=1);
function parsePositiveId(string $value): int
{
if (!ctype_digit($value)) {
throw new InvalidArgumentException('ID must contain only digits.');
}
$id = (int) $value;
if ($id <= 0) {
throw new InvalidArgumentException('ID must be positive.');
}
return $id;
}
try {
echo parsePositiveId('42') . PHP_EOL;
echo parsePositiveId('abc') . PHP_EOL;
} catch (InvalidArgumentException $exception) {
echo $exception->getMessage() . PHP_EOL;
}
// Prints:
// 42
// ID must contain only digits.
This catches the specific problem the code expects.
Catch Specific Exceptions When You Can
Specific catches make intent clearer.
<?php
declare(strict_types=1);
function decodeJsonObject(string $json): array
{
$data = json_decode($json, true, flags: JSON_THROW_ON_ERROR);
if (!is_array($data)) {
throw new RuntimeException('Expected a JSON object.');
}
return $data;
}
try {
decodeJsonObject('{broken');
} catch (JsonException $exception) {
echo 'Invalid JSON: ' . $exception->getMessage() . PHP_EOL;
}
The catch block handles JSON decoding failure. It does not pretend to recover from every possible bug in the program.
Error Is Usually A Bug
An Error is often something you should fix, not handle as normal control flow.
<?php
declare(strict_types=1);
function double(int $number): int
{
return $number * 2;
}
try {
double('not a number');
} catch (TypeError $error) {
echo 'Programming error: ' . $error->getMessage() . PHP_EOL;
}
This example catches the TypeError so you can see it, but production application code should usually prevent this with correct types and validation before the call.
Catching Throwable
Catching Throwable is useful at application boundaries: a front controller, CLI command runner, queue worker, or test harness. The boundary can log the failure, return a safe response, and keep sensitive details away from users.
<?php
declare(strict_types=1);
function runJob(): void
{
throw new RuntimeException('Email provider failed.');
}
try {
runJob();
echo 'Job finished.' . PHP_EOL;
} catch (Throwable $throwable) {
echo 'Job failed: ' . $throwable->getMessage() . PHP_EOL;
}
// Prints:
// Job failed: Email provider failed.
Do not catch Throwable deep inside normal business logic unless you have a clear recovery path. Broad catches can hide real bugs.
Rethrowing
Sometimes you catch an exception to add context, then throw another exception.
<?php
declare(strict_types=1);
function readConfig(string $path): string
{
$contents = @file_get_contents($path);
if ($contents === false) {
throw new RuntimeException('Could not read config file.');
}
return $contents;
}
try {
readConfig('/missing/config.php');
} catch (RuntimeException $exception) {
throw new RuntimeException('Application boot failed.', previous: $exception);
}
The previous exception preserves the original cause. This is valuable in logs and error trackers.
finally
A finally block runs whether the try block succeeds or fails. Use it for cleanup.
<?php
declare(strict_types=1);
function processImport(): void
{
echo 'Opening import file' . PHP_EOL;
try {
echo 'Processing rows' . PHP_EOL;
throw new RuntimeException('Row is invalid.');
} finally {
echo 'Closing import file' . PHP_EOL;
}
}
try {
processImport();
} catch (RuntimeException $exception) {
echo $exception->getMessage() . PHP_EOL;
}
// Prints:
// Opening import file
// Processing rows
// Closing import file
// Row is invalid.
Use finally for cleanup that must happen even after a failure.
Common Mistakes
Avoid catching Exception or Throwable and doing nothing. That hides failures and makes debugging much harder.
Avoid returning false, null, and throwing exceptions for the same failure without a clear rule. Pick a consistent style for the boundary.
Avoid showing raw exception messages to users when they may contain paths, SQL, tokens, or internal details. Log details privately and return safe public messages.
What You Should Be Able To Do
After this lesson, you should be able to explain the difference between Throwable, Exception, and Error, catch specific exceptions, use finally, preserve previous exceptions, and know where broad Throwable handling belongs.
For junior work, this matters because good exception handling makes failures visible without turning every bug into a silent mystery.
Practice
Practice: Handle Expected And Unexpected Failures
Create a small PHP example that validates an ID and catches the expected exception.
Task
Build:
- a
parsePositiveId()function - validation that rejects non-digits
- validation that rejects zero
- a specific
InvalidArgumentExceptioncatch - a boundary-level
Throwablecatch around a job runner
Use strict types. Keep the expected output in the PHP code block as printed lines or comments.
Check Your Work
Run cases for:
- a valid ID
- an invalid ID
- a job that throws a runtime exception
Afterward, explain why catching Throwable belongs at an application boundary rather than inside every small function.
Show solution
This solution catches the expected validation exception specifically, then shows a broader Throwable catch at a job boundary.
<?php
declare(strict_types=1);
function parsePositiveId(string $value): int
{
if (!ctype_digit($value)) {
throw new InvalidArgumentException('ID must contain only digits.');
}
$id = (int) $value;
if ($id <= 0) {
throw new InvalidArgumentException('ID must be positive.');
}
return $id;
}
function runJob(): void
{
throw new RuntimeException('Email provider failed.');
}
try {
echo parsePositiveId('42') . PHP_EOL;
echo parsePositiveId('abc') . PHP_EOL;
} catch (InvalidArgumentException $exception) {
echo $exception->getMessage() . PHP_EOL;
}
try {
runJob();
} catch (Throwable $throwable) {
echo 'Job failed: ' . $throwable->getMessage() . PHP_EOL;
}
// Prints:
// 42
// ID must contain only digits.
// Job failed: Email provider failed.
Catching Throwable at a boundary lets the application log or report any failure safely. Catching it everywhere inside small functions can hide programming errors and make broken code look successful.