There has been a trend over the past few years that concepts initially just expressed in PHP docblocks eventually become native PHP types. Past examples are: scalar typehints, return types, union types, mixed types, and static types.
Our static analysis tools currently provide support for @return noreturn
docblocks to denote functions that always throw
or exit
. Users of these tools have found that syntax useful to describe the behaviour of their own code, but we think it’s even more useful as a native return type, where PHP compile-time and runtime type-checks can guarantee its behaviour.
Introduce a noreturn
type that can be used in functions that always throw
or exit
.
Redirect functions that always call exit
(either explicitly or implicitly) are good candidates for such a return type:
function redirect(string $uri): noreturn { header('Location: ' . $uri); exit(); } function redirectToLoginPage(): noreturn { redirect('/login'); }
PHP developers can call these functions, safe in the knowledge that no statements after the function call will be evaluated:
function sayHello(?User $user) { if (!$user) { redirectToLoginPage(); } echo 'Hello ' . $user->getName(); }
If, at some later date, the redirect function is changed so that it does sometimes return a value, a compile error is produced:
function redirect(string $uri): noreturn { if ($uri === '') { return; // Fatal error: A noreturn function must not return } header('Location: ' . $uri); exit(); }
If, instead, the above function is rewritten to have an implicit return, a TypeError
is emitted:
function redirect(string $uri): noreturn { if ($uri !== '') { header('Location: ' . $uri); exit(); } } redirect(''); // Uncaught TypeError: redirect(): Nothing was expected to be returned
Attempting to use yield
inside a noreturn
function produces a compile-time error:
function generateList(string $uri): noreturn { yield 1; exit(); } // Fatal error: Generator return type must be a supertype of Generator
Like void
, the noreturn
type is only valid when used as a function return type. Using noreturn
as an argument or property type produces a compile-time error:
class A { public noreturn $x; // Fatal error }
In type theory noreturn
would be called a “bottom” type. That means it's effectively a subtype of every other type in PHP’s type system, including void
.
It obeys the rules you might expect of a universal subtype:
Return type covariance is allowed:
abstract class Person { abstract public function hasAgreedToTerms(): bool; } class Kid extends Person { public function hasAgreedToTerms(): noreturn { throw new \Exception('Kids cannot legally agree to terms'); } }
Return type contravariance is prohibited:
abstract class Redirector { abstract public function execute(): noreturn; } class BadRedirector extends Redirector { public function execute(): void {} // Fatal error }
Returning by reference with a noreturn
type is allowed as well.
class A { public function &test(): int { ... } } class B extends A { public function &test(): noreturn { throw new Exception; } }
Returning noreturn
is also allowed in __toString methods:
class A implements Stringable { public function __toString(): string { return "hello"; } } class B extends A { public function __toString(): noreturn { throw new \Exception('not supported'); } }
Since noreturn
is a subtype of all other types, a function that could be annotated with noreturn
can still safely be annotated with another return type:
function doFoo(): int { throw new \Exception(); }
In the absence of an explicit return type some PHP static analysis tools have also adopted support for noreturn
or similar:
/** @return noreturn */
#[JetBrains\PhpStorm\NoReturn]
Both noreturn
and void
are both only valid as return types, but there the similarity ends.
When you call a function that returns void
you generally expect PHP to execute the next statement after that function call.
function sayHello(string $name): void { echo "Hello $name"; } sayHello('World'); echo ", it’s nice to meet you";
But when you call a function that returns noreturn
you explicitly do not expect PHP to execute whatever statement follows:
function redirect(string $uri): noreturn { header('Location: ' . $uri); exit(); } redirect('/index.html'); echo "this will never be executed!";
Some might feel that noreturn
belongs as a function/method attribute, potentially a root-namespaced one:
Attribute form:
#[\NoReturn] function redirectToLoginPage(): void {...}
Type form:
function redirectToLoginPage(): noreturn {...}
We believe it’s more useful as a type. Internally PHP has a much more straightforward interpretation of return types than attributes, and PHP can quickly check variance rules for noreturn
types just as it does for void
. It's also tidier.
Naming is hard. We each have different preferences.
Arguments for noreturn
:
Arguments for never
:
noreturn
does not have any visual separator between the two words and one cannot be sensibly added e.g. no-return
.never
as a placeholder inside contravariant generic types.
noreturn
becomes a reserved word in PHP 8.1
8.1
Draft implementation here: https://github.com/php/php-src/pull/6761
Voting opens 2021-03-30 and 2021-04-13 at 11:00:00 AM EDT. 2/3 required to accept.
Following vote requires simple majority: