A failed API call, an invalid argument, or a database error can interrupt a PHP request at exactly the wrong moment. Without proper handling, the user may get a broken page while the useful error details disappear into a log somewhere.
PHP try-catch gives you a structured way to deal with these failures. Put code that may throw an exception inside try, handle the expected failure in catch, and use finally when cleanup must happen either way.
The important part is knowing what to catch and where. Catch too little and the exception escapes. Catch everything everywhere and debugging becomes its own exception.
How PHP try-catch works
A basic PHP try-catch block has two parts:
trycontains code that may throw an exception.catchruns when a thrown object matches the exception type it handles.
When PHP encounters throw, execution stops at that point inside the try block. PHP then looks for the first matching catch block. If no matching handler exists in the current function, the exception propagates up the call stack.
<?php
try {
throw new Exception('Something went wrong.');
} catch (Exception $exception) {
echo $exception->getMessage();
}
The output is:
Something went wrong.
A try block must be followed by at least one catch or finally block. A catch is therefore not mandatory when a finally block is present.
A practical PHP try-catch example
Exceptions are most useful when a function cannot complete its job and the caller needs to decide what happens next.
For example, this function expects a positive quantity:
<?php
function calculateTotal(float $price, int $quantity): float
{
if ($quantity <= 0) {
throw new InvalidArgumentException(
'Quantity must be greater than zero.'
);
}
return $price * $quantity;
}
try {
$total = calculateTotal(49.99, 0);
echo 'Total: ' . $total;
} catch (InvalidArgumentException $exception) {
echo $exception->getMessage();
}
Because the quantity is invalid, calculateTotal() throws an InvalidArgumentException. PHP skips the remaining statements in the try block and runs the matching catch.
This separation is useful in real applications. The function reports that it cannot continue, while the calling code decides whether to display a message, log the problem, return an API response, or take another action.
Using multiple catch blocks
A try block can have more than one catch. This is useful when different failures need different handling.
<?php
function processOrder(int $quantity, bool $paymentAvailable): string
{
if ($quantity <= 0) {
throw new InvalidArgumentException(
'Quantity must be greater than zero.'
);
}
if (!$paymentAvailable) {
throw new RuntimeException(
'Payment service is unavailable.'
);
}
return 'Order processed successfully.';
}
try {
echo processOrder(2, false);
} catch (InvalidArgumentException $exception) {
echo 'Invalid order: ' . $exception->getMessage();
} catch (RuntimeException $exception) {
echo 'Processing error: ' . $exception->getMessage();
}
PHP checks the catch blocks from top to bottom and runs the first compatible one.
This means catch order matters when exception types are related. Put the more specific exception before its parent class.
<?php
try {
throw new InvalidArgumentException('Invalid value.');
} catch (InvalidArgumentException $exception) {
echo 'Invalid argument';
} catch (Exception $exception) {
echo 'General exception';
}
InvalidArgumentException extends Exception. If the general Exception catch came first, it would also match the object and the more specific handler would never run.
Catch multiple exception types in one block
If several exception types should be handled in exactly the same way, PHP lets you combine them with the | operator.
<?php
try {
// Code that may throw either exception.
} catch (InvalidArgumentException | RuntimeException $exception) {
echo 'Request failed: ' . $exception->getMessage();
}
This is cleaner than duplicating identical catch blocks. Use separate catches only when the response to each exception is actually different.
Using finally in PHP
A finally block runs after try and catch, whether an exception was thrown or not.
<?php
try {
echo "Processing...\n";
throw new RuntimeException('The operation failed.');
} catch (RuntimeException $exception) {
echo $exception->getMessage() . "\n";
} finally {
echo 'Cleanup completed.';
}
The output is:
Processing...
The operation failed.
Cleanup completed.
finally is useful for cleanup work that should happen regardless of success or failure. For example, you may release a lock, close a temporary resource, or restore application state.
A finally block also runs when the exception is not caught locally and continues up the call stack.
Exception vs Error vs Throwable
In modern PHP, both Exception objects and many engine-level PHP errors implement the Throwable interface.
This gives you three common choices:
- Catch
Exceptionwhen you want to handle exceptions only. - Catch a specific exception class when you know the failure you expect.
- Catch
Throwablewhen you intentionally want to handle both exceptions and PHP errors.
<?php
try {
strlen([]);
} catch (TypeError $error) {
echo 'Type error: ' . $error->getMessage();
}
In PHP 8, passing an array to strlen() throws a PHP TypeError. It is not an Exception, so this would not be caught by catch (Exception $exception).
You can catch Throwable instead:
<?php
try {
strlen([]);
} catch (Throwable $throwable) {
echo $throwable->getMessage();
}
That does not mean every application-level catch should use Throwable. A broad catch can hide programming mistakes if you treat every failure as an expected condition. Prefer the most specific type you can handle meaningfully.
Rethrowing an exception
Sometimes a catch block needs to do part of the handling, such as logging, but should not consider the problem resolved.
In that case, catch the exception and throw it again:
<?php
function saveOrder(): void
{
try {
throw new RuntimeException('Could not save the order.');
} catch (RuntimeException $exception) {
error_log($exception->getMessage());
throw $exception;
}
}
try {
saveOrder();
} catch (RuntimeException $exception) {
echo 'Please try again later.';
}
The inner catch records the technical problem. The outer catch decides what the user should see.
This is often better than displaying the original exception message directly, especially when it may contain file paths, database details, SQL fragments, or other internal information.
Wrap an exception and keep the original cause
You may also want to replace a low-level exception with one that better describes the current operation.
<?php
function importFile(string $filename): void
{
try {
if (!is_readable($filename)) {
throw new RuntimeException('File cannot be read.');
}
// Import the file.
} catch (RuntimeException $exception) {
throw new RuntimeException(
'Import failed.',
0,
$exception
);
}
}
The third constructor argument stores the original exception as the previous exception. You can retrieve it later with getPrevious().
This keeps the useful low-level cause without forcing every higher-level caller to understand the details of the original failure.
Common PHP try-catch mistakes
Most problems with exception handling come from catching too broadly, catching too early, or hiding the failure completely.
Catching Exception before a specific exception
This order is wrong:
<?php
try {
throw new InvalidArgumentException('Invalid value.');
} catch (Exception $exception) {
echo 'General exception';
} catch (InvalidArgumentException $exception) {
echo 'Invalid argument';
}
InvalidArgumentException is already an Exception, so the first catch handles it. The specific catch below it can never run.
Put specific exception classes first and broader parent classes later.
Using an empty catch block
A catch block like this makes failures disappear:
<?php
try {
performOperation();
} catch (RuntimeException $exception) {
// Ignore it.
}
If the exception is genuinely safe to ignore, make that decision explicit in the code and comments. Otherwise, handle it, log it, or let it propagate.
Displaying raw exception messages to users
This is convenient during development:
<?php
try {
processRequest();
} catch (Throwable $throwable) {
echo $throwable->getMessage();
}
It is usually a poor choice for production. Exception messages can expose implementation details that users do not need to see.
Log the technical information and return a safe message instead:
<?php
try {
processRequest();
} catch (Throwable $throwable) {
error_log($throwable->getMessage());
echo 'The request could not be completed.';
}
Wrapping every line in try-catch
A try-catch block is not a replacement for normal validation or application logic.
For example, checking whether a required form field is empty usually does not need an exception. Exceptions are better for situations where an operation cannot complete normally and the failure needs to travel to another layer of the application.
Where should you catch an exception?
Catch an exception where you can make a useful decision about it.
A low-level function may know that an operation failed, but it may not know whether the application should retry, show an error page, return JSON, or abort a transaction. In that case, let the exception propagate to code that has enough context to decide.
<?php
function loadConfiguration(string $path): string
{
if (!is_readable($path)) {
throw new RuntimeException('Configuration file is not readable.');
}
$content = file_get_contents($path);
if ($content === false) {
throw new RuntimeException('Configuration file could not be loaded.');
}
return $content;
}
try {
$configuration = loadConfiguration('config/app.ini');
// Continue application startup.
} catch (RuntimeException $exception) {
error_log($exception->getMessage());
echo 'Application configuration could not be loaded.';
}
The function reports the failure. The calling code handles it at a level where it can decide what should happen next.
That pattern keeps exception handling useful instead of scattering catch blocks throughout every function.
PHP try-catch with custom exceptions
Built-in exception classes are enough for many cases. A custom exception becomes useful when the failure belongs to your application domain and callers may want to handle it differently.
<?php
class InsufficientStockException extends RuntimeException
{
}
function reserveStock(int $available, int $requested): void
{
if ($requested > $available) {
throw new InsufficientStockException(
'Not enough stock is available.'
);
}
}
try {
reserveStock(3, 5);
echo 'Stock reserved.';
} catch (InsufficientStockException $exception) {
echo $exception->getMessage();
}
The benefit is not the custom class name alone. It lets calling code distinguish this failure from unrelated runtime problems without inspecting message text.
Avoid creating a custom exception for every tiny variation. Add one when the distinction is useful to the code that catches it.
PHP try-catch FAQ
Does PHP continue after a catch block?
Yes. After the matching catch block finishes, execution continues with the next statement after the complete try-catch-finally structure.
If the exception is rethrown inside the catch, normal execution does not continue there.
Can PHP have try without catch?
Yes, if the try block is followed by finally.
<?php
try {
performOperation();
} finally {
releaseResource();
}
If performOperation() throws an exception, releaseResource() still runs. The exception then continues upward because nothing caught it.
Can one catch handle multiple exceptions?
Yes. Use the | operator when the exception types need identical handling.
<?php
try {
performOperation();
} catch (InvalidArgumentException | RuntimeException $exception) {
handleFailure($exception);
}
Should I catch Exception or Throwable?
Catch the most specific type you can handle properly.
Use Exception when you want exception objects only. Use Throwable when the code intentionally needs to handle both exceptions and PHP Error objects.
Using Throwable everywhere is usually too broad because it can also catch programming errors such as TypeError.
What happens if an exception is not caught?
PHP propagates it up the call stack looking for a compatible handler. If no handler catches it, it becomes an uncaught exception and normal execution terminates.
Conclusion
PHP try-catch is most effective when exceptions represent failures that another part of the application can meaningfully handle.
Use specific exception types where possible. Keep specific catch blocks before broader ones. Use finally for cleanup, rethrow failures that are not fully handled, and avoid exposing raw exception details to users.
The goal is not to catch everything. It is to catch failures at the place where your application can make the right decision about them.