Functions that are guaranteed to never return
void means a function does not produce a useful return value, but still returns to the caller normally. never means something fundamentally different: the function never returns control at all, it always throws an exception or terminates the script. Since PHP 8.1, that distinction can be expressed directly in the signature.
Table of Contents
- 1. What never means and how it differs from void
- 2. When a function should declare never instead of void
- 3. Syntax and rules: never cannot be combined
- 4. The benefit for static analysis
- 5. never in abstract methods and interfaces
- 6. Interaction with the throw expression since PHP 8.0
- 7. Common mistakes: never on functions that sometimes do return
- 8. Practical example: never in a router and dispatcher
- 9. never combined with exit and middleware stacks
- 10. Summary
- 11. FAQ
1. What never means and how it differs from void
void describes a function that does not produce a return value, but returns to the caller normally after execution, control flow simply continues right after the function call. never describes a function that never returns control at all: either it is guaranteed to throw an exception, or it terminates the process through exit or die.
That difference is not a mere formality, it changes what the caller is allowed to assume about the code after the function call. After a void call, the following code still has to account for every possible state, after a never call, the following code is simply unreachable from the type system's point of view.
This distinction exists in many statically typed languages under different names, for example as a bottom type or never type, and describes the same concept there: a type for which there can by definition be no valid instance and no normal return from a function.
2. When a function should declare never instead of void
never belongs in a signature whenever a function is guaranteed, without exception, to either throw an exception or terminate the script through exit or die, with no code path that returns normally instead. Even a single path with a regular return makes never the wrong choice.
Typical candidates are pure error handling functions like throwNotFound(), assertion helpers that always throw on a failed condition, or bootstrap scripts that immediately terminate the program on a fatal configuration error.
function throwNotFound(string $resource): never
{
throw new NotFoundException("Resource not found: {$resource}");
}
3. Syntax and rules: never cannot be combined
Unlike most types, never cannot be combined with other types into a union type, because never already represents the empty set of all possible return values. An expression like never|string makes no logical sense and is rejected by the parser.
A function declared as never that actually has a path with a regular return, whether with or without a value, causes a fatal error at runtime as soon as that path executes. The compiler does not statically verify that guarantee for completeness, only external analysis tools do.
// Fatal error at runtime once this path executes: never must not return
function validate(int $value): never
{
if ($value < 0) {
throw new InvalidArgumentException('Value must be non-negative');
}
return; // invalid: this path returns normally
}
4. The benefit for static analysis
The real value of never does not show up at runtime, it shows up in tools like PHPStan and Psalm: as soon as an analysis recognizes a call to a never function, it marks every piece of code right after it as unreachable and reports it, instead of silently ignoring it.
That is especially valuable for type narrowing: if a variable is narrowed to a specific type right before a never call, the analysis tool can reliably assume, after the call, that the excluded case no longer exists at all, because control flow never leaves that branch normally.
function process(?User $user): void
{
if ($user === null) {
throwNotFound('user');
}
// PHPStan narrows $user to User here, the null case is provably unreachable
echo $user->getName();
}
5. never in abstract methods and interfaces
never can also be used in abstract methods and interface declarations to mandate that every implementation must never return normally. That is useful for contracts like a FatalHandlerInterface, where every concrete implementation is guaranteed to either throw an exception or terminate the program.
Importantly, an implementation may narrow the return type, but never widen it, so never in an interface signature forces every implementation to also use never, or something even more specific, deviating to void would be a contract violation and a type compatibility error.
6. Interaction with the throw expression since PHP 8.0
Since PHP 8.0, throw is an expression rather than a plain statement, which allows using throw inside a null coalescing expression or a ternary operator, for example. never complements that feature at the function level, attributing the same guarantee to an entire function that throw provides for a single expression.
Combining both features lets you build a compact helper function that is itself declared never and internally uses throw as an expression to throw different kinds of errors depending on a condition, without multi line if blocks.
7. Common mistakes: never on functions that sometimes do return
The most common mistake with never happens when a function is meant to always throw in theory, but has an overlooked path in practice, for example a switch statement without a default branch that simply falls through on an unexpected value and implicitly returns null.
These mistakes stay undetected at compile time and only show up as a fatal error at runtime once the missing path actually executes. A match expression instead of switch reduces this risk noticeably, because match automatically throws an UnhandledMatchError on a missing case instead of silently falling through.
function dispatch(string $action): never
{
match ($action) {
'delete' => throw new ForbiddenException('Action not allowed'),
'legacy' => exit('Legacy action terminated'),
// No default needed: match throws UnhandledMatchError automatically
};
}
8. Practical example: never in a router and dispatcher
In a simple router, never fits naturally on the method that reacts to a missing route. Instead of modeling a 404 response as a regular return value, the method throws a specific exception, which an outer error handler turns into an actual HTTP response.
The benefit shows up directly in the calling code: after calling notFound(), no additional check is needed, neither a return value check nor nullable handling, because the type system itself guarantees that this path never continues normally.
final class Router
{
public function dispatch(string $path): Response
{
foreach ($this->routes as $route) {
if ($route->matches($path)) {
return $route->handle();
}
}
$this->notFound($path); // never returns, no code after this needs a null check
}
private function notFound(string $path): never
{
throw new RouteNotFoundException("No route matches: {$path}");
}
}
9. never combined with exit and middleware stacks
Besides exceptions, exit or die is the second legitimate way to satisfy a never guarantee, for example in a CLI script that should terminate immediately with an exit code on a fatal configuration error, without the rest of the call stack continuing.
In middleware stacks, caution is needed: a middleware declared never permanently interrupts the entire processing chain, there is no way for subsequent middleware to still intervene. never should therefore only be used for truly terminal failure cases, not for regular control flow decisions inside a pipeline.
A useful thought experiment in practice: anyone wondering whether a middleware really should never return normally should check whether there is a single conceivable production case where the request could still be processed further despite the failure, because even one such case argues against never.
| Feature | void | never |
|---|---|---|
| Does the function return? | Yes, without a return value | No, never |
| Code after the call | Keeps executing normally | Considered unreachable |
| Combinable with union types | Yes, in certain contexts | No, never combinable |
| Typical use case | Setters, logging methods | Error handling, exit, assertion helpers |
| Error on violation | No error, void tolerates any lack of return value | Fatal error on a regular return |
| Benefit for type narrowing | No special effect | Analysis tools reliably exclude the case |
Mironsoft
PHP modernization, code quality, and legacy refactoring
Grown PHP code nobody wants to touch anymore?
We modernize PHP codebases to current language standards, introduce static analysis and coding standards, and refactor legacy code step by step without endangering live operations.
Legacy Refactoring
Modernize grown PHP code in a structured, low-risk way.
Establishing Code Quality
Anchor PHPStan, coding standards, and CI checks sustainably in the team.
Version Upgrades
Plan and execute PHP major version upgrades safely, without downtime.
10. Summary
never Return Type
Core idea
never signals that a function is guaranteed to never return control to the caller normally.
Distinction
void returns without a value, never does not return at all, it throws or terminates the process.
Benefit
PHPStan and Psalm reliably mark code after a never call as unreachable.
Caution
A single regular return path in a never function causes a fatal error at runtime.