Documentation

Checked errors

Make recoverable failures visible in callable contracts while preserving ordinary PHP exception behavior.

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 makes the recoverable error set part of the compile-time contract.

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

A caller must remove every checked error from the escaping set by catching it, or publish the remaining error in its own clause:

function completeCheckout(
    Checkout $checkout,
    PaymentGateway $gateway,
): Receipt throws GatewayUnavailable
{
    try {
        return charge(
            $gateway,
            $checkout->paymentMethod,
            $checkout->total,
        );
    } catch (CardDeclined $error) {
        throw new CheckoutDeclined($checkout->id, $error);
    }
}

A matching catch handles the declared type and its subtypes. In this example, GatewayUnavailable is not handled, so the enclosing function declares it. If CheckoutDeclined is a checked exception thrown by the catch body, it must also be caught or declared; translating an error does not make it disappear from the contract.

Error sets flow through the whole callable

The compiler combines errors from:

  • explicit throw expressions;
  • calls to ++PHP functions, methods, and constructors;
  • callbacks whose callable contract declares errors;
  • branch and loop control flow;
  • ordinary PHP declarations described by PHPDoc or stubs.

When paths join, the escaping error set is the union of failures that can reach that point. A catch removes the handled type and its subtypes from the protected region.

A callable with no throws clause promises that no known checked error escapes. It does not promise that the PHP runtime, an extension, memory exhaustion, or an unresolved dynamic boundary can never produce a Throwable.

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.