rfc:noreturn_type

PHP RFC: noreturn type

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

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.

Add noreturn type
Real name Yes No
alcaeus (alcaeus)  
alec (alec)  
asgrim (asgrim)  
ashnazg (ashnazg)  
beberlei (beberlei)  
bmajdak (bmajdak)  
brzuchal (brzuchal)  
carusogabriel (carusogabriel)  
crell (crell)  
danack (danack)  
daverandom (daverandom)  
derick (derick)  
dharman (dharman)  
dragoonis (dragoonis)  
duncan3dc (duncan3dc)  
galvao (galvao)  
gasolwu (gasolwu)  
girgias (girgias)  
heiglandreas (heiglandreas)  
ilutov (ilutov)  
jasny (jasny)  
jbnahan (jbnahan)  
jhdxr (jhdxr)  
kalle (kalle)  
kelunik (kelunik)  
kguest (kguest)  
kocsismate (kocsismate)  
krakjoe (krakjoe)  
lstrojny (lstrojny)  
marandall (marandall)  
mariano (mariano)  
mauricio (mauricio)  
mbeccati (mbeccati)  
mfonda (mfonda)  
narf (narf)  
nicolasgrekas (nicolasgrekas)  
nikic (nikic)  
ocramius (ocramius)  
patrickallaert (patrickallaert)  
pierrick (pierrick)  
pmjones (pmjones)  
pollita (pollita)  
ramsey (ramsey)  
reywob (reywob)  
sebastian (sebastian)  
seld (seld)  
sergey (sergey)  
svpernova09 (svpernova09)  
tandre (tandre)  
theodorejb (theodorejb)  
thorstenr (thorstenr)  
trowski (trowski)  
twosee (twosee)  
Final result: 42 11
This poll has been closed.

Following vote requires simple majority:

noreturn vs never
Real name noreturn never
alcaeus (alcaeus)  
alec (alec)  
asgrim (asgrim)  
ashnazg (ashnazg)  
beberlei (beberlei)  
bmajdak (bmajdak)  
brzuchal (brzuchal)  
carusogabriel (carusogabriel)  
crell (crell)  
daverandom (daverandom)  
derick (derick)  
dharman (dharman)  
dragoonis (dragoonis)  
duncan3dc (duncan3dc)  
galvao (galvao)  
gasolwu (gasolwu)  
girgias (girgias)  
heiglandreas (heiglandreas)  
ilutov (ilutov)  
jasny (jasny)  
jbnahan (jbnahan)  
jhdxr (jhdxr)  
kalle (kalle)  
kelunik (kelunik)  
kguest (kguest)  
kocsismate (kocsismate)  
krakjoe (krakjoe)  
levim (levim)  
lstrojny (lstrojny)  
marandall (marandall)  
mariano (mariano)  
mauricio (mauricio)  
mbeccati (mbeccati)  
mfonda (mfonda)  
narf (narf)  
nicolasgrekas (nicolasgrekas)  
ocramius (ocramius)  
patrickallaert (patrickallaert)  
pollita (pollita)  
ramsey (ramsey)  
reywob (reywob)  
seld (seld)  
sergey (sergey)  
svpernova09 (svpernova09)  
tandre (tandre)  
theodorejb (theodorejb)  
thorstenr (thorstenr)  
trowski (trowski)  
Final result: 14 34
This poll has been closed.
rfc/noreturn_type.txt · Last modified: 2021/04/13 21:50 by mattbrown