This is an old revision of the document!
PHP RFC: noreturn type
- Version: 0.1
- Date: 2021-03-10
- Author: Matt Brown php@muglug.com & Ondřej Mirtes ondrej@mirtes.cz
- Status: Under Discussion
- 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:
<code php>
class A implements Stringable {
public function toString(): string {
return “hello”;
}
}
class B extends A {
public function toString(): noreturn {
throw new \Exception('not supported');
}
}
</code>
=== 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:
<code php>
function doFoo(): int
{
throw new \Exception();
}
</code>
==== 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.
<code php>
function sayHello(string $name): void {
echo “Hello $name”;
}
sayHello('World');
echo “, it’s nice to meet you”;
</code>
But when you call a function that returns
noreturn you explicitly do not expect PHP to execute whatever statement follows:
<code php>
function redirect(string $uri): noreturn {
header('Location: ' . $uri);
exit();
}
redirect('/index.html');
echo “this will never be executed!”;
</code>
==== Attributes vs types ====
Some might feel that
noreturn belongs as a function/method attribute, potentially a root-namespaced one:
Attribute form:
<code php>
#[\NoReturn]
function redirectToLoginPage(): void {...}
</code>
Type form:
<code php>
function redirectToLoginPage(): noreturn {...}
</code>
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, but we believe
noreturn is the best name for this type.
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
===== Proposed Voting Choices =====
Yes/no vote for adding
noreturn (2/3 majority)
Vote for
noreturn vs
never'' (simple majority)