Skip to content

Commit 04e5cfc

Browse files
authored
Merge pull request #93 from reactphp/enforce-exception-reasons
Enforce throwables/exceptions as rejection reasons
2 parents c2608dd + 5f67f82 commit 04e5cfc

22 files changed

+249
-322
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,7 @@ CHANGELOG for 3.x
4444
\React\Promise\resolve($promise)->cancel();
4545
}
4646
```
47+
* BC break: When rejecting a promise, a `\Throwable` or `\Exception`
48+
instance is enforced as the rejection reason (#93).
49+
This means, it is not longer possible to reject a promise without a reason
50+
or with another promise.

README.md

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ $deferred = new React\Promise\Deferred();
9393
$promise = $deferred->promise();
9494

9595
$deferred->resolve(mixed $value = null);
96-
$deferred->reject(mixed $reason = null);
96+
$deferred->reject(\Throwable|\Exception $reason);
9797
```
9898

9999
The `promise` method returns the promise of the deferred.
@@ -128,17 +128,14 @@ this promise once it is resolved.
128128
#### Deferred::reject()
129129

130130
```php
131-
$deferred->reject(mixed $reason = null);
131+
$deferred->reject(\Throwable|\Exception $reason);
132132
```
133133

134134
Rejects the promise returned by `promise()`, signalling that the deferred's
135135
computation failed.
136136
All consumers are notified by having `$onRejected` (which they registered via
137137
`$promise->then()`) called with `$reason`.
138138

139-
If `$reason` itself is a promise, the promise will be rejected with the outcome
140-
of this promise regardless whether it fulfills or rejects.
141-
142139
### PromiseInterface
143140

144141
The promise interface provides the common interface for all promise
@@ -359,8 +356,7 @@ Creates a already rejected promise.
359356
$promise = React\Promise\RejectedPromise($reason);
360357
```
361358

362-
Note, that `$reason` **cannot** be a promise. It's recommended to use
363-
[reject()](#reject) for creating rejected promises.
359+
Note, that `$reason` **must** be a `\Throwable` or `\Exception`.
364360

365361
### Functions
366362

@@ -390,20 +386,10 @@ If `$promiseOrValue` is a promise, it will be returned as is.
390386
#### reject()
391387

392388
```php
393-
$promise = React\Promise\reject(mixed $promiseOrValue);
389+
$promise = React\Promise\reject(\Throwable|\Exception $reason);
394390
```
395391

396-
Creates a rejected promise for the supplied `$promiseOrValue`.
397-
398-
If `$promiseOrValue` is a value, it will be the rejection value of the
399-
returned promise.
400-
401-
If `$promiseOrValue` is a promise, its completion value will be the rejected
402-
value of the returned promise.
403-
404-
This can be useful in situations where you need to reject a promise without
405-
throwing an exception. For example, it allows you to propagate a rejection with
406-
the value of another promise.
392+
Creates a rejected promise for the supplied `$reason`.
407393

408394
#### all()
409395

@@ -439,7 +425,9 @@ Returns a promise that will resolve when any one of the items in
439425
will be the resolution value of the triggering item.
440426

441427
The returned promise will only reject if *all* items in `$promisesOrValues` are
442-
rejected. The rejection value will be an array of all rejection reasons.
428+
rejected. The rejection value will be a `React\Promise\Exception\CompositeException`
429+
which holds all rejection reasons. The rejection reasons can be obtained with
430+
`CompositeException::getExceptions()`.
443431

444432
The returned promise will also reject with a `React\Promise\Exception\LengthException`
445433
if `$promisesOrValues` contains 0 items.
@@ -457,8 +445,9 @@ triggering items.
457445

458446
The returned promise will reject if it becomes impossible for `$howMany` items
459447
to resolve (that is, when `(count($promisesOrValues) - $howMany) + 1` items
460-
reject). The rejection value will be an array of
461-
`(count($promisesOrValues) - $howMany) + 1` rejection reasons.
448+
reject). The rejection value will be a `React\Promise\Exception\CompositeException`
449+
which holds `(count($promisesOrValues) - $howMany) + 1` rejection reasons.
450+
The rejection reasons can be obtained with `CompositeException::getExceptions()`.
462451

463452
The returned promise will also reject with a `React\Promise\Exception\LengthException`
464453
if `$promisesOrValues` contains less items than `$howMany`.
@@ -503,7 +492,7 @@ function getAwesomeResultPromise()
503492
$deferred = new React\Promise\Deferred();
504493

505494
// Execute a Node.js-style function using the callback pattern
506-
computeAwesomeResultAsynchronously(function ($error, $result) use ($deferred) {
495+
computeAwesomeResultAsynchronously(function (\Exception $error, $result) use ($deferred) {
507496
if ($error) {
508497
$deferred->reject($error);
509498
} else {
@@ -520,7 +509,7 @@ getAwesomeResultPromise()
520509
function ($value) {
521510
// Deferred resolved, do something with $value
522511
},
523-
function ($reason) {
512+
function (\Exception $reason) {
524513
// Deferred rejected, do something with $reason
525514
}
526515
);
@@ -701,11 +690,6 @@ getJsonResult()
701690
);
702691
```
703692

704-
Note that if a rejection value is not an instance of `\Exception`, it will be
705-
wrapped in an exception of the type `React\Promise\UnhandledRejectionException`.
706-
707-
You can get the original rejection reason by calling `$exception->getReason()`.
708-
709693
Credits
710694
-------
711695

src/Deferred.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public function resolve($value = null)
3636
\call_user_func($this->resolveCallback, $value);
3737
}
3838

39-
public function reject($reason = null)
39+
public function reject($reason)
4040
{
4141
$this->promise();
4242

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace React\Promise\Exception;
4+
5+
/**
6+
* Represents an exception that is a composite of one or more other exceptions.
7+
*
8+
* This exception is useful in situations where a promise must be rejected
9+
* with multiple exceptions. It is used for example to reject the returned
10+
* promise from `some()` and `any()` when too many input promises reject.
11+
*/
12+
class CompositeException extends \Exception
13+
{
14+
private $exceptions;
15+
16+
public function __construct(array $exceptions, $message = '', $code = 0, $previous = null)
17+
{
18+
parent::__construct($message, $code, $previous);
19+
20+
$this->exceptions = $exceptions;
21+
}
22+
23+
/**
24+
* @return \Throwable[]|\Exception[]
25+
*/
26+
public function getExceptions()
27+
{
28+
return $this->exceptions;
29+
}
30+
}

src/Promise.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ private function resolve($value = null)
129129
$this->settle(resolve($value));
130130
}
131131

132-
private function reject($reason = null)
132+
private function reject($reason)
133133
{
134134
if (null !== $this->result) {
135135
return;
@@ -198,7 +198,7 @@ private function call(callable $callback)
198198
function ($value = null) {
199199
$this->resolve($value);
200200
},
201-
function ($reason = null) {
201+
function ($reason) {
202202
$this->reject($reason);
203203
}
204204
);

src/RejectedPromise.php

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,16 @@ final class RejectedPromise implements PromiseInterface
66
{
77
private $reason;
88

9-
public function __construct($reason = null)
9+
public function __construct($reason)
1010
{
11-
if ($reason instanceof PromiseInterface) {
12-
throw new \InvalidArgumentException('You cannot create React\Promise\RejectedPromise with a promise. Use React\Promise\reject($promiseOrValue) instead.');
11+
if (!$reason instanceof \Throwable && !$reason instanceof \Exception) {
12+
throw new \InvalidArgumentException(
13+
sprintf(
14+
'A Promise must be rejected with a \Throwable or \Exception instance, got "%s" instead.',
15+
is_object($reason) ? get_class($reason) : gettype($reason)
16+
)
17+
18+
);
1319
}
1420

1521
$this->reason = $reason;
@@ -38,9 +44,7 @@ public function done(callable $onFulfilled = null, callable $onRejected = null)
3844
{
3945
enqueue(function () use ($onRejected) {
4046
if (null === $onRejected) {
41-
return fatalError(
42-
UnhandledRejectionException::resolve($this->reason)
43-
);
47+
return fatalError($this->reason);
4448
}
4549

4650
try {
@@ -52,9 +56,7 @@ public function done(callable $onFulfilled = null, callable $onRejected = null)
5256
}
5357

5458
if ($result instanceof self) {
55-
return fatalError(
56-
UnhandledRejectionException::resolve($result->reason)
57-
);
59+
return fatalError($result->reason);
5860
}
5961

6062
if ($result instanceof PromiseInterface) {

src/UnhandledRejectionException.php

Lines changed: 0 additions & 31 deletions
This file was deleted.

src/functions.php

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace React\Promise;
44

5+
use React\Promise\Exception\CompositeException;
6+
57
/**
68
* Creates a promise for the supplied `$promiseOrValue`.
79
*
@@ -16,6 +18,7 @@
1618
* @param mixed $promiseOrValue
1719
* @return PromiseInterface
1820
*/
21+
1922
function resolve($promiseOrValue = null)
2023
{
2124
if ($promiseOrValue instanceof PromiseInterface) {
@@ -53,15 +56,9 @@ function resolve($promiseOrValue = null)
5356
* @param mixed $promiseOrValue
5457
* @return PromiseInterface
5558
*/
56-
function reject($promiseOrValue = null)
59+
function reject($reason)
5760
{
58-
if ($promiseOrValue instanceof PromiseInterface) {
59-
return resolve($promiseOrValue)->then(function ($value) {
60-
return new RejectedPromise($value);
61-
});
62-
}
63-
64-
return new RejectedPromise($promiseOrValue);
61+
return new RejectedPromise($reason);
6562
}
6663

6764
/**
@@ -199,7 +196,12 @@ function some(array $promisesOrValues, $howMany)
199196
$reasons[$i] = $reason;
200197

201198
if (0 === --$toReject) {
202-
$reject($reasons);
199+
$reject(
200+
new CompositeException(
201+
$reasons,
202+
'Too many promises rejected.'
203+
)
204+
);
203205
}
204206
};
205207

tests/FunctionAllTest.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,16 @@ public function shouldResolveSparseArrayInput()
5959
/** @test */
6060
public function shouldRejectIfAnyInputPromiseRejects()
6161
{
62+
$exception2 = new \Exception();
63+
$exception3 = new \Exception();
64+
6265
$mock = $this->createCallableMock();
6366
$mock
6467
->expects($this->once())
6568
->method('__invoke')
66-
->with($this->identicalTo(2));
69+
->with($this->identicalTo($exception2));
6770

68-
all([resolve(1), reject(2), resolve(3)])
71+
all([resolve(1), reject($exception2), resolve($exception3)])
6972
->then($this->expectCallableNever(), $mock);
7073
}
7174

tests/FunctionAnyTest.php

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace React\Promise;
44

5+
use React\Promise\Exception\CompositeException;
56
use React\Promise\Exception\LengthException;
67

78
class FunctionAnyTest extends TestCase
@@ -53,26 +54,38 @@ public function shouldResolveWithAPromisedInputValue()
5354
/** @test */
5455
public function shouldRejectWithAllRejectedInputValuesIfAllInputsAreRejected()
5556
{
57+
$exception1 = new \Exception();
58+
$exception2 = new \Exception();
59+
$exception3 = new \Exception();
60+
61+
$compositeException = new CompositeException(
62+
[0 => $exception1, 1 => $exception2, 2 => $exception3],
63+
'Too many promises rejected.'
64+
);
65+
5666
$mock = $this->createCallableMock();
5767
$mock
5868
->expects($this->once())
5969
->method('__invoke')
60-
->with($this->identicalTo([0 => 1, 1 => 2, 2 => 3]));
70+
->with($compositeException);
6171

62-
any([reject(1), reject(2), reject(3)])
72+
any([reject($exception1), reject($exception2), reject($exception3)])
6373
->then($this->expectCallableNever(), $mock);
6474
}
6575

6676
/** @test */
6777
public function shouldResolveWhenFirstInputPromiseResolves()
6878
{
79+
$exception2 = new \Exception();
80+
$exception3 = new \Exception();
81+
6982
$mock = $this->createCallableMock();
7083
$mock
7184
->expects($this->once())
7285
->method('__invoke')
7386
->with($this->identicalTo(1));
7487

75-
any([resolve(1), reject(2), reject(3)])
88+
any([resolve(1), reject($exception2), reject($exception3)])
7689
->then($mock);
7790
}
7891

0 commit comments

Comments
 (0)