Language

when expressions

Calculate, validate, or loop inside a conditional branch before returning its result.

An if statement controls what happens next. Sometimes the decision itself is the value you need: a shipping quote, a permission level, or the response returned from a service. PHP's ternary operator and match expression are excellent when each result is already a direct expression. A different shape is needed when a branch must validate input, calculate intermediate values, record an event, or iterate over data before it can produce the result.

when gives those branches room to work while requiring the entire decision to produce one compatible result. Initialize a variable directly from that decision instead of declaring a placeholder and assigning it separately in each arm of an if chain. Use a guard clause when there is only one useful result and the other path throws.

Produce a value from realistic branches

ShippingQuote $quote = when ($basket->isEmpty()) {
    return new ShippingQuote('free', $rates->zero);
} else when ($customer->hasPriorityDelivery()) {
    Money $fee = $rates->priorityFeeFor($basket);
    $audit->recordPriorityQuote($customer, $fee);

    return new ShippingQuote('priority', $fee);
} else {
    Money $fee = $rates->standardFeeFor($basket);

    return new ShippingQuote('standard', $fee);
};

Each branch is a block, so it can declare branch-local values and perform meaningful work before yielding. The return belongs to the when expression; it does not return from the enclosing function.

The surrounding declaration establishes the expected result type. Every reachable branch must produce a ShippingQuote or a compatible subtype.

Provide a result with a final else

A value-producing decision cannot leave the destination uninitialized. A final else is therefore mandatory:

ShippingQuote $quote = when ($customer->hasPriorityDelivery()) {
    Money $fee = $rates->priorityFeeFor($basket);
    $audit->recordPriorityQuote($customer, $fee);

    return new ShippingQuote('priority', $fee);
} else when ($customer->acceptsPickup()) {
    Store $store = $stores->nearestTo($customer->address);
    $audit->recordPickupQuote($customer, $store);

    return ShippingQuote::pickup($store);
}; // rejected: other customers still need a result

A branch can throw instead of returning a value when it cannot produce a useful result:

Customer $customer = when ($record !== null) {
    ValidatedCustomerRecord $valid = $validator->validate($record);

    return Customer::fromRecord($valid);
} else {
    $audit->recordMissingCustomer($customerId);

    throw new CustomerNotFound($customerId);
};

Here, a missing record throws CustomerNotFound; otherwise, $customer receives the validated customer. No path continues with an uninitialized $customer.

Evaluation is predictable

Conditions are evaluated from left to right and at most once. Evaluation stops at the first matching branch, just as it does in an if / else if / else chain. PHP truthiness and comparison semantics remain unchanged.

Variables declared in a branch stay inside that branch:

string $message = when ($response->isSuccessful()) {
    string $reference = $response->reference;

    return 'Accepted: ' . $reference;
} else {
    string $reason = $response->failureReason;

    return 'Declined: ' . $reason;
};

// $reference and $reason are not available here.

Loop and return early inside a branch

Use continue to skip an item and break to stop a loop inside a branch. After that loop finishes, the branch still needs to return a value or throw. If you have already found the branch's result, use return $result instead: it ends the whole when, including any loops nested inside it.

Both transfers must stay inside the when. Numbered forms such as break 2 can target nested loops or switches within the branch, but cannot escape to a loop outside it. A continue must target a loop, not a switch. Inside finally, neither transfer may jump out of that finally block.

Returns inside a nested function or closure belong to that callable, not the surrounding when.

Handle failures and cleanup

Use try, catch, and finally inside a branch as you would elsewhere. Cleanup runs before the result is assigned. If cleanup throws, the exception propagates and an existing destination keeps its previous value.

A return in finally can replace a value returned earlier in the branch, but it never cancels a pending exception or error. Catch a failure explicitly when you want to recover and produce a value. If finally itself throws, its exception takes precedence.

Cleanup also runs before a pending break or continue. If that cleanup returns a when result, the expression finishes with that result instead of resuming the loop transfer. Otherwise the transfer continues to its original target.

Use when where a value is expected

++PHP supports when in the positions where an expression naturally supplies data:

  • a typed local initializer;
  • the right-hand side of a later assignment;
  • the value returned by a function or method;
  • a direct call argument;
  • an array element.
return when ($request->acceptsJson()) {
    array<string, mixed> $payload = $presenter->forJson($request);
    $audit->recordResponseFormat('json');

    return Response::json($payload);
} else {
    string $html = $presenter->forHtml($request);
    $audit->recordResponseFormat('html');

    return Response::html($html);
};

For a call argument whose branches contain statements, use a function or method with an explicit signature and explicit preceding arguments. A call through an unresolved callable, or an unpacked argument before that when, can produce P5005 because the compiler cannot establish how the arguments are passed. A two-branch choice containing only bare result expressions keeps PHP's normal argument handling. Known by-reference parameters cannot receive a when result.

Loop headers, echo statements, defaults, constants, and attributes do not accept when. Compute the value in a supported statement first, then use it. If the computation must happen on every iteration, keep it inside the loop body rather than moving it before the loop.

Use a normal if when the goal is control flow or side effects and no single value is produced. Use a ternary for a short two-expression choice. Use match when every arm maps directly to an expression. Use when when one or more branches need a readable block of preparation before returning the typed result.

See the generated PHP

In this example, each branch prepares a message and returns it directly into the declaration. The generated PHP assigns $message in ordinary if and else branches:

string $message = when ($payment->isSettled()) {
    string $reference = $payment->reference;
    $audit->recordSettlement($reference);

    return 'Paid: ' . $reference;
} else {
    string $reason = $payment->pendingReason;
    $audit->recordPendingPayment($reason);

    return 'Pending: ' . $reason;
};

becomes:

if ($payment->isSettled()) {
    /** @var string $reference */
    $reference = $payment->reference;
    $audit->recordSettlement($reference);
    $message = 'Paid: ' . $reference;
} else {
    /** @var string $reason */
    $reason = $payment->pendingReason;
    $audit->recordPendingPayment($reason);
    $message = 'Pending: ' . $reason;
}

Diagnostics continue to point to the original when expression, while the generated file remains readable PHP.

Return compatible types from each branch

When assigning a when result to an Order variable, return an Order from every branch that completes normally. A branch returning a string does not satisfy that declaration. If both types are useful to the caller, declare Order|string and check which type you received before using it.

Handle checked errors from calls inside a branch just as you would elsewhere: catch them or declare them on the enclosing function. Variables declared inside a branch follow the typed local rules and are available only in that branch.