Language
Checked errors
Declare the failures callers should handle, catch them where you can recover, and pass them on when you cannot.A return type describes success. Many application operations also have expected failure modes: a payment can be declined, a customer can be missing, or a storage service can be unavailable. If those outcomes are documented only in prose, a new caller can forget to handle them and still pass review.
A throws clause lists those failures beside the function's return type, so callers can see both the result to expect and the errors to handle.
Declare the failures a caller must consider
function charge(
PaymentGateway $gateway,
PaymentMethod $method,
Money $amount,
): Receipt throws CardDeclined, GatewayUnavailable
{
return $gateway->charge($method, $amount);
}
The function can return Receipt, throw CardDeclined, or throw GatewayUnavailable. PHP still throws and catches the same exception objects. The clause lets the compiler verify that every caller handles or declares them.
Catch or propagate
Catch an error when this function can handle it. Otherwise, add it to the function's throws clause so the next caller knows to handle it:
function completeCheckout(
Checkout $checkout,
PaymentGateway $gateway,
): Receipt throws GatewayUnavailable, CheckoutDeclined
{
try {
return charge(
$gateway,
$checkout->paymentMethod,
$checkout->total,
);
} catch (CardDeclined $error) {
throw new CheckoutDeclined($checkout->id, $error);
}
}
A matching catch handles the named exception class and its subclasses. Here, completeCheckout lets GatewayUnavailable reach its caller and replaces CardDeclined with CheckoutDeclined. Both errors that can reach the caller appear in its throws clause. The caller can then retry an unavailable gateway or show the customer a declined-checkout message.
Handle errors from the code you call
Your function is responsible for checked errors from all the work it performs, including:
- explicit
throwexpressions; - calls to ++PHP functions, methods, and constructors;
- callbacks whose callable contract declares errors;
- calls and throws inside branches and loops;
- ordinary PHP declarations described by PHPDoc or stubs.
Calling a function inside an if or a loop does not remove the need to handle its errors. Put the call inside a suitable try/catch, or declare the error on your own function. The same applies when a callback or constructor can throw.
A function with no throws clause must handle its known checked errors before they reach the caller. Keep your normal application-level error handling too: runtime failures such as memory exhaustion are not recoverable business errors described by this clause.
Distinguish absence, rejection, and programmer failure
Use a nullable return when “not found” is an ordinary answer the caller can branch on:
function findCoupon(string $code): ?Coupon
{
// Absence is expected data.
}
Use a checked error when the operation could not fulfil its contract and the caller must choose a recovery path:
function loadInvoice(string $id): Invoice throws StorageUnavailable
{
// The storage dependency prevented an answer.
}
PHP's Error hierarchy remains unchecked. Type errors, calling inaccessible members, and other programming failures are not turned into recoverable business cases merely because PHP represents them as throwables.
Implementations may promise fewer errors
An implementation can promise fewer failures than its interface, but it cannot surprise callers with a new checked failure:
interface CustomerStore
{
public function load(string $id): Customer
throws CustomerNotFound, StorageUnavailable;
}
final class CachedCustomerStore implements CustomerStore
{
public function load(string $id): Customer
throws CustomerNotFound
{
// A cache-backed implementation removes StorageUnavailable.
}
}
The reverse would be unsafe: code written against the interface would have no reason to handle the additional error.
Cross ordinary PHP boundaries
Accurate @throws PHPDoc and configured stubs contribute error information for existing PHP APIs:
/**
* @throws TransportFailure
*/
function legacy_send(Request $request): Response
{
// Existing PHP implementation.
}
If the compiler cannot determine a callable's declared errors, it reports that the call cannot be verified. Add a stub when the API is stable but its declarations are not otherwise available to the project.
See what ships
The throws clause is erased and preserved as PHPDoc:
function loadUser(string $id): User throws UserNotFound
{
// ...
}
emits conceptually:
/**
* @throws UserNotFound
*/
function loadUser(string $id): User
{
// ...
}
At runtime, throw, try, catch, exception inheritance, stack traces, and propagation are PHP's. Existing PHP callers can inspect the metadata, while ++PHP callers receive compile-time enforcement.
Design useful error contracts
Keep clauses focused on failures callers can meaningfully recover from. Prefer domain errors such as CardDeclined over leaking every low-level transport exception through the application. Translate errors at architectural boundaries, document the cause when useful, and let implementations narrow their promises.
Read when expressions for value-producing recovery choices, or Mixed projects for bringing PHPDoc error contracts into a gradual migration.