PHP RFC: noreturn type
- Version: 0.1
- Date: 2021-03-10
- Author: Matt Brown php@muglug.com & Ondřej Mirtes ondrej@mirtes.cz
- Status: Implemented
- Proposed Version: PHP 8.1
- Implementation: https://github.com/php/php-src/pull/6761
Introduction
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.
Proposal
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
Applicability
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 }
Variance
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'); } }
Allowed return types when a function always throws
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(); }
Prior art in other interpreted languages
- Hacklang has a noreturn type.
- TypeScript has a never type that's also an explicit bottom type.
- Python has a NoReturn type as part of its official typing library.
Prior art in PHP static analysis tools
In the absence of an explicit return type some PHP static analysis tools have also adopted support for noreturn
or similar:
- Psalm and PHPStan support the docblock return type
/** @return noreturn */
- PHPStorm supports a custom PHP 8 attribute
#[JetBrains\PhpStorm\NoReturn]
Comparison to void
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!";
Attributes vs types
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
Naming is hard. We each have different preferences.
Arguments for noreturn
:
- Very unlikely to be used as an existing class name.
- Describes the behaviour of the function.
Arguments for never
:
- It's a single word -
noreturn
does not have any visual separator between the two words and one cannot be sensibly added e.g.no-return
. - It's a full-fledged type, rather than a keyword used in a specific situation. A far-in-the-future generics proposal could use
never
as a placeholder inside contravariant generic types.
Backwards Incompatible Changes
noreturn
becomes a reserved word in PHP 8.1
Proposed PHP Version(s)
8.1
Patches and Tests
Draft implementation here: https://github.com/php/php-src/pull/6761
Vote
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: