Table of Contents

PHP RFC: throw expression

Introduction

Since in PHP throw is a statement it makes it impossible to throw exceptions in places where only expressions are allowed, like arrow functions, the coalesce operator and the ternary/elvis operator. This RFC proposes converting the throw statement into an expression so that these cases become possible.

Proposal

Allow using throw in any context where an expression is accepted. These examples come to mind that seem useful:

// This was previously not possible since arrow functions only accept a single expression while throw was a statement.
$callable = fn() => throw new Exception();
 
// $value is non-nullable.
$value = $nullableValue ?? throw new InvalidArgumentException();
 
// $value is truthy.
$value = $falsableValue ?: throw new InvalidArgumentException();
 
// $value is only set if the array is not empty.
$value = !empty($array)
    ? reset($array)
    : throw new InvalidArgumentException();

There are other places where it could be used which are more controversial. These cases are allowed in this RFC.

// An if statement could make the intention clearer
$condition && throw new Exception();
$condition || throw new Exception();
$condition and throw new Exception();
$condition or throw new Exception();

Operator precedence

If throw becomes an expression operator precedence becomes relevant. These examples are working today.

throw $this->createNotFoundException();
// Evaluated as
throw ($this->createNotFoundException());
// Instead of
(throw $this)->createNotFoundException();
 
throw static::createNotFoundException();
// Evaluated as
throw (static::createNotFoundException());
// Instead of
(throw static)::createNotFoundException();
 
throw $userIsAuthorized ? new ForbiddenException() : new UnauthorizedException();
// Evaluated as
throw ($userIsAuthorized ? new ForbiddenException() : new UnauthorizedException());
// Instead of
(throw $userIsAuthorized) ? new ForbiddenException() : new UnauthorizedException();
 
throw $maybeNullException ?? new Exception();
// Evaluated as
throw ($maybeNullException ?? new Exception());
// Instead of
(throw $maybeNullException) ?? new Exception();
 
throw $exception = new Exception();
// Evaluated as
throw ($exception = new Exception());
// Instead of
(throw $exception) = new Exception();
 
throw $exception ??= new Exception();
// Evaluated as
throw ($exception ??= new Exception());
// Instead of
(throw $exception) ??= new Exception();
 
throw $condition1 && $condition2 ? new Exception1() : new Exception2();
// Evaluated as
throw ($condition1 && $condition2 ? new Exception1() : new Exception2());
// Instead of
(throw $condition1) && $condition2 ? new Exception1() : new Exception2();

The common theme here is that everything after the throw keyword has a higher precedence. For this reason this RFC proposes to use the lowest operator precedence possible. All the current code, even if broken or strange, will continue behaving the same way. This isn't a problem because generally throw should be the last operator you're using as every expression after it wouldn't be evaluated anyway.

The only downside of the low precedence is that a throw between two short-circuit operators would not be possible without parentheses:

$condition || throw new Exception('$condition must be truthy')
  && $condition2 || throw new Exception('$condition2 must be truthy');
// Evaluated as
$condition || (throw new Exception('$condition must be truthy') && $condition2 || (throw new Exception('$condition2 must be truthy')));
// Instead of
$condition || (throw new Exception('$condition must be truthy'))
  && $condition2 || (throw new Exception('$condition2 must be truthy'));

But I see little use for code like this.

Backward Incompatible Changes

None, specifically because the lowest precedence was chosen.

Other languages

The same was implemented in C# 7.0 in 2017. 1)

There aren't many other languages that allow this. There's an ECMAScript proposal because the same issues exist there. 2)

Proposed PHP Version(s)

Proposed version is PHP 8.

Voting

Voting starts 2020-04-05 and ends 2020-04-19.

As this is a language change, a 2/3 majority is required. The vote is a straight Yes/No vote for accepting the RFC and merging the patch.

Would you like to convert the throw statement into an expression?
Real name Yes No
ajf (ajf)  
alcaeus (alcaeus)  
as (as)  
ashnazg (ashnazg)  
beberlei (beberlei)  
brzuchal (brzuchal)  
bukka (bukka)  
bwoebi (bwoebi)  
carusogabriel (carusogabriel)  
colinodell (colinodell)  
danack (danack)  
daverandom (daverandom)  
derick (derick)  
dragoonis (dragoonis)  
duncan3dc (duncan3dc)  
duodraco (duodraco)  
ekin (ekin)  
galvao (galvao)  
geekcom (geekcom)  
jasny (jasny)  
jhdxr (jhdxr)  
kalle (kalle)  
kguest (kguest)  
krakjoe (krakjoe)  
lcobucci (lcobucci)  
levim (levim)  
marandall (marandall)  
mariano (mariano)  
nicolasgrekas (nicolasgrekas)  
nikic (nikic)  
ocramius (ocramius)  
petk (petk)  
pierrick (pierrick)  
pmjones (pmjones)  
pmmaga (pmmaga)  
ralphschindler (ralphschindler)  
ramsey (ramsey)  
rasmus (rasmus)  
reywob (reywob)  
salathe (salathe)  
sammyk (sammyk)  
sergey (sergey)  
stas (stas)  
svpernova09 (svpernova09)  
tandre (tandre)  
thekid (thekid)  
wyrihaximus (wyrihaximus)  
yunosh (yunosh)  
zimt (zimt)  
Final result: 46 3
This poll has been closed.

References

Old discussion in internals list