diff --git a/README.md b/README.md
index 7bfb2382..64516b2a 100644
--- a/README.md
+++ b/README.md
@@ -125,6 +125,8 @@ having `$onFulfilled` (which they registered via `$promise->then()`) called with
If `$value` itself is a promise, the promise will transition to the state of
this promise once it is resolved.
+See also the [`resolve()` function](#resolve).
+
#### Deferred::reject()
```php
@@ -136,6 +138,8 @@ computation failed.
All consumers are notified by having `$onRejected` (which they registered via
`$promise->then()`) called with `$reason`.
+See also the [`reject()` function](#reject).
+
### PromiseInterface
The promise interface provides the common interface for all promise
@@ -361,6 +365,19 @@ a trusted promise that follows the state of the thenable is returned.
If `$promiseOrValue` is a promise, it will be returned as is.
+The resulting `$promise` implements the [`PromiseInterface`](#promiseinterface)
+and can be consumed like any other promise:
+
+```php
+$promise = React\Promise\resolve(42);
+
+$promise->then(function (int $result): void {
+ var_dump($result);
+}, function (\Throwable $e): void {
+ echo 'Error: ' . $e->getMessage() . PHP_EOL;
+});
+```
+
#### reject()
```php
@@ -374,6 +391,52 @@ both user land [`\Exception`](https://www.php.net/manual/en/class.exception.php)
[`\Error`](https://www.php.net/manual/en/class.error.php) internal PHP errors. By enforcing `\Throwable` as reason to
reject a promise, any language error or user land exception can be used to reject a promise.
+The resulting `$promise` implements the [`PromiseInterface`](#promiseinterface)
+and can be consumed like any other promise:
+
+```php
+$promise = React\Promise\reject(new RuntimeException('Request failed'));
+
+$promise->then(function (int $result): void {
+ var_dump($result);
+}, function (\Throwable $e): void {
+ echo 'Error: ' . $e->getMessage() . PHP_EOL;
+});
+```
+
+Note that rejected promises should always be handled similar to how any
+exceptions should always be caught in a `try` + `catch` block. If you remove the
+last reference to a rejected promise that has not been handled, it will
+report an unhandled promise rejection:
+
+```php
+function incorrect(): int
+{
+ $promise = React\Promise\reject(new RuntimeException('Request failed'));
+
+ // Commented out: No rejection handler registered here.
+ // $promise->then(null, function (\Throwable $e): void { /* ignore */ });
+
+ // Returning from a function will remove all local variable references, hence why
+ // this will report an unhandled promise rejection here.
+ return 42;
+}
+
+// Calling this function will log an error message plus its stack trace:
+// Unhandled promise rejection with RuntimeException: Request failed in example.php:10
+incorrect();
+```
+
+A rejected promise will be considered "handled" if you catch the rejection
+reason with either the [`then()` method](#promiseinterfacethen), the
+[`catch()` method](#promiseinterfacecatch), or the
+[`finally()` method](#promiseinterfacefinally). Note that each of these methods
+return a new promise that may again be rejected if you re-throw an exception.
+
+A rejected promise will also be considered "handled" if you abort the operation
+with the [`cancel()` method](#promiseinterfacecancel) (which in turn would
+usually reject the promise if it is still pending).
+
#### all()
```php
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index 895c8410..293865dd 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -4,3 +4,7 @@ parameters:
paths:
- src/
- tests/
+
+ fileExtensions:
+ - php
+ - phpt
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 42422782..bba7c900 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -10,6 +10,7 @@
./tests/
+ ./tests/
diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy
index 5a312e5d..4bdf7718 100644
--- a/phpunit.xml.legacy
+++ b/phpunit.xml.legacy
@@ -8,6 +8,7 @@
./tests/
+ ./tests/
diff --git a/src/Internal/RejectedPromise.php b/src/Internal/RejectedPromise.php
index cbd8ef53..a29cc92d 100644
--- a/src/Internal/RejectedPromise.php
+++ b/src/Internal/RejectedPromise.php
@@ -14,6 +14,9 @@ final class RejectedPromise implements PromiseInterface
/** @var \Throwable */
private $reason;
+ /** @var bool */
+ private $handled = false;
+
/**
* @param \Throwable $reason
*/
@@ -22,12 +25,26 @@ public function __construct(\Throwable $reason)
$this->reason = $reason;
}
+ public function __destruct()
+ {
+ if ($this->handled) {
+ return;
+ }
+
+ $message = 'Unhandled promise rejection with ' . \get_class($this->reason) . ': ' . $this->reason->getMessage() . ' in ' . $this->reason->getFile() . ':' . $this->reason->getLine() . PHP_EOL;
+ $message .= 'Stack trace:' . PHP_EOL . $this->reason->getTraceAsString();
+
+ \error_log($message);
+ }
+
public function then(callable $onFulfilled = null, callable $onRejected = null): PromiseInterface
{
if (null === $onRejected) {
return $this;
}
+ $this->handled = true;
+
try {
return resolve($onRejected($this->reason));
} catch (\Throwable $exception) {
@@ -55,6 +72,7 @@ public function finally(callable $onFulfilledOrRejected): PromiseInterface
public function cancel(): void
{
+ $this->handled = true;
}
/**
diff --git a/src/Promise.php b/src/Promise.php
index a2d72b6d..819e414a 100644
--- a/src/Promise.php
+++ b/src/Promise.php
@@ -18,6 +18,9 @@ final class Promise implements PromiseInterface
/** @var int */
private $requiredCancelRequests = 0;
+ /** @var bool */
+ private $cancelled = false;
+
public function __construct(callable $resolver, callable $canceller = null)
{
$this->canceller = $canceller;
@@ -89,12 +92,18 @@ public function finally(callable $onFulfilledOrRejected): PromiseInterface
public function cancel(): void
{
+ $this->cancelled = true;
$canceller = $this->canceller;
$this->canceller = null;
$parentCanceller = null;
if (null !== $this->result) {
+ // Forward cancellation to rejected promise to avoid reporting unhandled rejection
+ if ($this->result instanceof RejectedPromise) {
+ $this->result->cancel();
+ }
+
// Go up the promise chain and reach the top most promise which is
// itself not following another promise
$root = $this->unwrap($this->result);
@@ -191,6 +200,11 @@ private function settle(PromiseInterface $result): void
foreach ($handlers as $handler) {
$handler($result);
}
+
+ // Forward cancellation to rejected promise to avoid reporting unhandled rejection
+ if ($this->cancelled && $result instanceof RejectedPromise) {
+ $result->cancel();
+ }
}
private function unwrap(PromiseInterface $promise): PromiseInterface
diff --git a/src/functions.php b/src/functions.php
index c42b715e..c8107f8d 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -100,7 +100,7 @@ function (\Throwable $reason) use (&$continue, $reject): void {
}
);
- if (!$continue) {
+ if (!$continue && !\is_array($promisesOrValues)) {
break;
}
}
@@ -136,7 +136,7 @@ function race(iterable $promisesOrValues): PromiseInterface
$continue = false;
});
- if (!$continue) {
+ if (!$continue && !\is_array($promisesOrValues)) {
break;
}
}
@@ -187,7 +187,7 @@ function (\Throwable $reason) use ($i, &$reasons, &$toReject, $reject, &$continu
}
);
- if (!$continue) {
+ if (!$continue && !\is_array($promisesOrValues)) {
break;
}
}
diff --git a/tests/DeferredTest.php b/tests/DeferredTest.php
index bb8f08d6..186ed1d7 100644
--- a/tests/DeferredTest.php
+++ b/tests/DeferredTest.php
@@ -54,9 +54,13 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenc
gc_collect_cycles();
gc_collect_cycles(); // clear twice to avoid leftovers in PHP 7.4 with ext-xdebug and code coverage turned on
+ /** @var Deferred $deferred */
$deferred = new Deferred(function () use (&$deferred) {
assert($deferred instanceof Deferred);
});
+
+ $deferred->promise()->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+
$deferred->reject(new \Exception('foo'));
unset($deferred);
diff --git a/tests/DeferredTestCancelNoopThenRejectShouldNotReportUnhandled.phpt b/tests/DeferredTestCancelNoopThenRejectShouldNotReportUnhandled.phpt
new file mode 100644
index 00000000..e4e1d64d
--- /dev/null
+++ b/tests/DeferredTestCancelNoopThenRejectShouldNotReportUnhandled.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Calling cancel() and then reject() should not report unhandled rejection
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+promise()->cancel();
+$deferred->reject(new RuntimeException('foo'));
+
+echo 'void' . PHP_EOL;
+
+?>
+--EXPECT--
+void
diff --git a/tests/DeferredTestCancelThatRejectsShouldNotReportUnhandled.phpt b/tests/DeferredTestCancelThatRejectsShouldNotReportUnhandled.phpt
new file mode 100644
index 00000000..d581a062
--- /dev/null
+++ b/tests/DeferredTestCancelThatRejectsShouldNotReportUnhandled.phpt
@@ -0,0 +1,20 @@
+--TEST--
+Calling cancel() that rejects should not report unhandled rejection
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+promise()->cancel();
+
+echo 'void' . PHP_EOL;
+
+?>
+--EXPECT--
+void
diff --git a/tests/DeferredTestRejectShouldReportUnhandled.phpt b/tests/DeferredTestRejectShouldReportUnhandled.phpt
new file mode 100644
index 00000000..46d8afa6
--- /dev/null
+++ b/tests/DeferredTestRejectShouldReportUnhandled.phpt
@@ -0,0 +1,20 @@
+--TEST--
+Calling reject() without any handlers should report unhandled rejection
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+reject(new RuntimeException('foo'));
+
+?>
+--EXPECTF--
+Unhandled promise rejection with RuntimeException: foo in %s:%d
+Stack trace:
+#0 %A{main}
diff --git a/tests/DeferredTestRejectThenCancelShouldNotReportUnhandled.phpt b/tests/DeferredTestRejectThenCancelShouldNotReportUnhandled.phpt
new file mode 100644
index 00000000..ecda961a
--- /dev/null
+++ b/tests/DeferredTestRejectThenCancelShouldNotReportUnhandled.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Calling reject() and then cancel() should not report unhandled rejection
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+reject(new RuntimeException('foo'));
+$deferred->promise()->cancel();
+
+echo 'void' . PHP_EOL;
+
+?>
+--EXPECT--
+void
diff --git a/tests/FunctionAllTest.php b/tests/FunctionAllTest.php
index 94b5c049..10cfa2ff 100644
--- a/tests/FunctionAllTest.php
+++ b/tests/FunctionAllTest.php
@@ -106,7 +106,7 @@ public function shouldRejectIfAnyInputPromiseRejects(): void
->method('__invoke')
->with(self::identicalTo($exception2));
- all([resolve(1), reject($exception2), resolve($exception3)])
+ all([resolve(1), reject($exception2), reject($exception3)])
->then($this->expectCallableNever(), $mock);
}
diff --git a/tests/FunctionAllTestRejectedShouldReportUnhandled.phpt b/tests/FunctionAllTestRejectedShouldReportUnhandled.phpt
new file mode 100644
index 00000000..b4ee4c9f
--- /dev/null
+++ b/tests/FunctionAllTestRejectedShouldReportUnhandled.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Calling all() with rejected promises should report unhandled rejection
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+
+--EXPECTF--
+Unhandled promise rejection with RuntimeException: foo in %s:%d
+Stack trace:
+#0 %A{main}
diff --git a/tests/FunctionAllTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt b/tests/FunctionAllTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt
new file mode 100644
index 00000000..c69bfeac
--- /dev/null
+++ b/tests/FunctionAllTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Calling all() and then then() should not report unhandled rejection
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+then(null, function (\Throwable $e) {
+ echo 'Handled ' . get_class($e) . ': ' . $e->getMessage() . PHP_EOL;
+});
+
+?>
+--EXPECT--
+Handled RuntimeException: foo
diff --git a/tests/FunctionAnyTestRejectedShouldReportUnhandled.phpt b/tests/FunctionAnyTestRejectedShouldReportUnhandled.phpt
new file mode 100644
index 00000000..935518ae
--- /dev/null
+++ b/tests/FunctionAnyTestRejectedShouldReportUnhandled.phpt
@@ -0,0 +1,28 @@
+--TEST--
+Calling any() with rejected promises should report unhandled rejection
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+
+--EXPECTF--
+Unhandled promise rejection with React\Promise\Exception\CompositeException: All promises rejected. in %s:%d
+Stack trace:
+#0 %s/src/Promise.php(%d): React\Promise\{closure}(%S)
+#1 %s/src/Promise.php(%d): React\Promise\Promise->call(%S)
+#2 %s/src/functions.php(%d): React\Promise\Promise->__construct(%S)
+#3 %s(%d): React\Promise\any(%S)
+#4 %A{main}
+
diff --git a/tests/FunctionAnyTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt b/tests/FunctionAnyTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt
new file mode 100644
index 00000000..840e3a78
--- /dev/null
+++ b/tests/FunctionAnyTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Calling any() and then then() should not report unhandled rejection
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+then(null, function (\Throwable $e) {
+ echo 'Handled ' . get_class($e) . ': ' . $e->getMessage() . PHP_EOL;
+});
+
+?>
+--EXPECT--
+Handled React\Promise\Exception\CompositeException: All promises rejected.
diff --git a/tests/FunctionRaceTestRejectedShouldReportUnhandled.phpt b/tests/FunctionRaceTestRejectedShouldReportUnhandled.phpt
new file mode 100644
index 00000000..de963d33
--- /dev/null
+++ b/tests/FunctionRaceTestRejectedShouldReportUnhandled.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Calling race() with rejected promises should report unhandled rejection
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+
+--EXPECTF--
+Unhandled promise rejection with RuntimeException: foo in %s:%d
+Stack trace:
+#0 %A{main}
diff --git a/tests/FunctionRaceTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt b/tests/FunctionRaceTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt
new file mode 100644
index 00000000..110f5f43
--- /dev/null
+++ b/tests/FunctionRaceTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Calling race() and then then() should not report unhandled rejection
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+then(null, function (\Throwable $e) {
+ echo 'Handled ' . get_class($e) . ': ' . $e->getMessage() . PHP_EOL;
+});
+
+?>
+--EXPECT--
+Handled RuntimeException: foo
diff --git a/tests/FunctionRejectTestCancelShouldNotReportUnhandled.phpt b/tests/FunctionRejectTestCancelShouldNotReportUnhandled.phpt
new file mode 100644
index 00000000..c6f17d5b
--- /dev/null
+++ b/tests/FunctionRejectTestCancelShouldNotReportUnhandled.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Calling reject() and then cancel() should not report unhandled rejection
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+cancel();
+
+echo 'void' . PHP_EOL;
+
+?>
+--EXPECT--
+void
diff --git a/tests/FunctionRejectTestCatchMatchingShouldNotReportUnhandled.phpt b/tests/FunctionRejectTestCatchMatchingShouldNotReportUnhandled.phpt
new file mode 100644
index 00000000..2fe0f207
--- /dev/null
+++ b/tests/FunctionRejectTestCatchMatchingShouldNotReportUnhandled.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Calling reject() and then catch() with matching type should not report unhandled rejection
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+catch(function (RuntimeException $e): void {
+ echo 'Handled ' . get_class($e) . ': ' . $e->getMessage() . PHP_EOL;
+});
+
+?>
+--EXPECT--
+Handled RuntimeException: foo
diff --git a/tests/FunctionRejectTestCatchMismatchShouldReportUnhandled.phpt b/tests/FunctionRejectTestCatchMismatchShouldReportUnhandled.phpt
new file mode 100644
index 00000000..a90a5499
--- /dev/null
+++ b/tests/FunctionRejectTestCatchMismatchShouldReportUnhandled.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Calling reject() and then catch() with mismatched type should report unhandled rejection
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+catch(function (UnexpectedValueException $unexpected): void {
+ echo 'This will never be shown because the types do not match' . PHP_EOL;
+});
+
+?>
+--EXPECTF--
+Unhandled promise rejection with RuntimeException: foo in %s:%d
+Stack trace:
+#0 %A{main}
diff --git a/tests/FunctionRejectTestFinallyThatReturnsShouldReportUnhandled.phpt b/tests/FunctionRejectTestFinallyThatReturnsShouldReportUnhandled.phpt
new file mode 100644
index 00000000..1f2e576f
--- /dev/null
+++ b/tests/FunctionRejectTestFinallyThatReturnsShouldReportUnhandled.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Calling reject() and then finally() should call handler and report unhandled rejection
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+finally(function (): void {
+ echo 'Foo' . PHP_EOL;
+});
+
+?>
+--EXPECTF--
+Foo
+Unhandled promise rejection with RuntimeException: foo in %s:%d
+Stack trace:
+#0 %A{main}
diff --git a/tests/FunctionRejectTestFinallyThatThrowsNewExceptionShouldReportUnhandledForNewExceptionOnly.phpt b/tests/FunctionRejectTestFinallyThatThrowsNewExceptionShouldReportUnhandledForNewExceptionOnly.phpt
new file mode 100644
index 00000000..6e6eaa93
--- /dev/null
+++ b/tests/FunctionRejectTestFinallyThatThrowsNewExceptionShouldReportUnhandledForNewExceptionOnly.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Calling reject() and then finally() should call handler and report unhandled rejection for new exception from handler
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+finally(function (): void {
+ throw new \RuntimeException('Finally!');
+});
+
+?>
+--EXPECTF--
+Unhandled promise rejection with RuntimeException: Finally! in %s:%d
+Stack trace:
+#0 %s/src/Internal/RejectedPromise.php(%d): {closure}(%S)
+#1 %s/src/Internal/RejectedPromise.php(%d): React\Promise\Internal\RejectedPromise->React\Promise\Internal\{closure}(%S)
+#2 %s/src/Internal/RejectedPromise.php(%d): React\Promise\Internal\RejectedPromise->then(%S)
+#3 %s(%d): React\Promise\Internal\RejectedPromise->finally(%S)
+#4 %A{main}
diff --git a/tests/FunctionRejectTestShouldReportUnhandled.phpt b/tests/FunctionRejectTestShouldReportUnhandled.phpt
new file mode 100644
index 00000000..9f2d7c77
--- /dev/null
+++ b/tests/FunctionRejectTestShouldReportUnhandled.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Calling reject() without any handlers should report unhandled rejection
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+
+--EXPECTF--
+Unhandled promise rejection with RuntimeException: foo in %s:%d
+Stack trace:
+#0 %A{main}
diff --git a/tests/FunctionRejectTestThenMatchingThatReturnsShouldNotReportUnhandled.phpt b/tests/FunctionRejectTestThenMatchingThatReturnsShouldNotReportUnhandled.phpt
new file mode 100644
index 00000000..90f6c42a
--- /dev/null
+++ b/tests/FunctionRejectTestThenMatchingThatReturnsShouldNotReportUnhandled.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Calling reject() and then then() should not report unhandled rejection
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+then(null, function (\Throwable $e) {
+ echo 'Handled ' . get_class($e) . ': ' . $e->getMessage() . PHP_EOL;
+});
+
+?>
+--EXPECT--
+Handled RuntimeException: foo
diff --git a/tests/FunctionRejectTestThenMatchingThatThrowsNewExceptionShouldReportUnhandledRejectionForNewExceptionOnly.phpt b/tests/FunctionRejectTestThenMatchingThatThrowsNewExceptionShouldReportUnhandledRejectionForNewExceptionOnly.phpt
new file mode 100644
index 00000000..b6b7f2fe
--- /dev/null
+++ b/tests/FunctionRejectTestThenMatchingThatThrowsNewExceptionShouldReportUnhandledRejectionForNewExceptionOnly.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Calling reject() and then then() should report unhandled rejection for new exception from handler
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+then(null, function () {
+ throw new \RuntimeException('bar');
+});
+
+?>
+--EXPECTF--
+Unhandled promise rejection with RuntimeException: bar in %s:%d
+Stack trace:
+#0 %s/src/Internal/RejectedPromise.php(%d): {closure}(%S)
+#1 %s(%d): React\Promise\Internal\RejectedPromise->then(%S)
+#2 %A{main}
diff --git a/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp7.phpt b/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp7.phpt
new file mode 100644
index 00000000..7a0cf9a1
--- /dev/null
+++ b/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp7.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Calling reject() and then then() with invalid type should report unhandled rejection for TypeError
+--SKIPIF--
+= 80000) die("Skipped: PHP 7 only."); ?>
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+then(null, function (UnexpectedValueException $unexpected): void {
+ echo 'This will never be shown because the types do not match' . PHP_EOL;
+});
+
+?>
+--EXPECTF--
+Unhandled promise rejection with TypeError: Argument 1 passed to {closure}() must be an instance of UnexpectedValueException, instance of RuntimeException given, called in %s/src/Internal/RejectedPromise.php on line %d in %s:%d
+Stack trace:
+#0 %s/src/Internal/RejectedPromise.php(%d): {closure}(%S)
+#1 %s(%d): React\Promise\Internal\RejectedPromise->then(%S)
+#2 %A{main}
diff --git a/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp8.phpt b/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp8.phpt
new file mode 100644
index 00000000..c8694f34
--- /dev/null
+++ b/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp8.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Calling reject() and then then() with invalid type should report unhandled rejection for TypeError
+--SKIPIF--
+
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+then(null, function (UnexpectedValueException $unexpected): void {
+ echo 'This will never be shown because the types do not match' . PHP_EOL;
+});
+
+?>
+--EXPECTF--
+Unhandled promise rejection with TypeError: {closure}(): Argument #1 ($unexpected) must be of type UnexpectedValueException, RuntimeException given, called in %s/src/Internal/RejectedPromise.php on line %d in %s:%d
+Stack trace:
+#0 %s/src/Internal/RejectedPromise.php(%d): {closure}(%S)
+#1 %s(%d): React\Promise\Internal\RejectedPromise->then(%S)
+#2 %A{main}
diff --git a/tests/FunctionResolveTest.php b/tests/FunctionResolveTest.php
index a1637b53..1eacd344 100644
--- a/tests/FunctionResolveTest.php
+++ b/tests/FunctionResolveTest.php
@@ -128,6 +128,10 @@ function ($val) {
/** @test */
public function shouldSupportVeryDeepNestedPromises(): void
{
+ if (PHP_VERSION_ID < 70200 && ini_get('xdebug.max_nesting_level') !== false) {
+ $this->markTestSkipped('Skip unhandled rejection on legacy PHP 7.1');
+ }
+
$deferreds = [];
for ($i = 0; $i < 150; $i++) {
diff --git a/tests/FunctionResolveTestThenShouldNotReportUnhandled.phpt b/tests/FunctionResolveTestThenShouldNotReportUnhandled.phpt
new file mode 100644
index 00000000..aabeae16
--- /dev/null
+++ b/tests/FunctionResolveTestThenShouldNotReportUnhandled.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Calling resolve() and then then() should not report unhandled rejection
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+then('var_dump');
+
+?>
+--EXPECT--
+int(42)
diff --git a/tests/PromiseTest.php b/tests/PromiseTest.php
index ae6b92bc..06d89eb1 100644
--- a/tests/PromiseTest.php
+++ b/tests/PromiseTest.php
@@ -66,6 +66,9 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptio
$promise = new Promise(function () {
throw new \Exception('foo');
});
+
+ $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+
unset($promise);
$this->assertSame(0, gc_collect_cycles());
@@ -78,6 +81,9 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverRejectsWithExc
$promise = new Promise(function ($resolve, $reject) {
$reject(new \Exception('foo'));
});
+
+ $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+
unset($promise);
$this->assertSame(0, gc_collect_cycles());
@@ -116,6 +122,9 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptio
$promise = new Promise(function ($resolve, $reject) {
throw new \Exception('foo');
});
+
+ $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+
unset($promise);
$this->assertSame(0, gc_collect_cycles());
@@ -137,6 +146,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptio
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReferenceThrowsException(): void
{
gc_collect_cycles();
+ /** @var Promise $promise */
$promise = new Promise(function () {}, function () use (&$promise) {
assert($promise instanceof Promise);
throw new \Exception('foo');
@@ -155,10 +165,14 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReference
public function shouldRejectWithoutCreatingGarbageCyclesIfResolverWithReferenceThrowsException(): void
{
gc_collect_cycles();
+ /** @var Promise $promise */
$promise = new Promise(function () use (&$promise) {
assert($promise instanceof Promise);
throw new \Exception('foo');
});
+
+ $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+
unset($promise);
$this->assertSame(0, gc_collect_cycles());
@@ -172,11 +186,15 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverWithReferenceT
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenceAndResolverThrowsException(): void
{
gc_collect_cycles();
+ /** @var Promise $promise */
$promise = new Promise(function () {
throw new \Exception('foo');
}, function () use (&$promise) {
assert($promise instanceof Promise);
});
+
+ $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+
unset($promise);
$this->assertSame(0, gc_collect_cycles());
@@ -260,6 +278,9 @@ public function shouldFulfillIfFullfilledWithSimplePromise(): void
$promise = new Promise(function () {
throw new Exception('foo');
});
+
+ $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+
unset($promise);
self::assertSame(0, gc_collect_cycles());
diff --git a/tests/PromiseTest/PromiseRejectedTestTrait.php b/tests/PromiseTest/PromiseRejectedTestTrait.php
index af8bcaf7..12287ce8 100644
--- a/tests/PromiseTest/PromiseRejectedTestTrait.php
+++ b/tests/PromiseTest/PromiseRejectedTestTrait.php
@@ -261,7 +261,7 @@ public function catchShouldNotInvokeRejectionHandlerIfReaonsDoesNotMatchTypehint
$adapter->promise()
->catch(function (InvalidArgumentException $reason) use ($mock) {
$mock($reason);
- });
+ })->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
}
/** @test */
@@ -461,7 +461,7 @@ public function otherwiseShouldNotInvokeRejectionHandlerIfReaonsDoesNotMatchType
$adapter->promise()
->otherwise(function (InvalidArgumentException $reason) use ($mock) {
$mock($reason);
- });
+ })->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
}
/**
diff --git a/tests/PromiseTest/PromiseSettledTestTrait.php b/tests/PromiseTest/PromiseSettledTestTrait.php
index 03ded7e0..3f63dd5d 100644
--- a/tests/PromiseTest/PromiseSettledTestTrait.php
+++ b/tests/PromiseTest/PromiseSettledTestTrait.php
@@ -2,6 +2,7 @@
namespace React\Promise\PromiseTest;
+use React\Promise\Internal\RejectedPromise;
use React\Promise\PromiseAdapter\PromiseAdapterInterface;
use React\Promise\PromiseInterface;
@@ -16,6 +17,10 @@ public function thenShouldReturnAPromiseForSettledPromise(): void
$adapter->settle(null);
self::assertInstanceOf(PromiseInterface::class, $adapter->promise()->then());
+
+ if ($adapter->promise() instanceof RejectedPromise) {
+ $adapter->promise()->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+ }
}
/** @test */
@@ -25,6 +30,10 @@ public function thenShouldReturnAllowNullForSettledPromise(): void
$adapter->settle(null);
self::assertInstanceOf(PromiseInterface::class, $adapter->promise()->then(null, null));
+
+ if ($adapter->promise() instanceof RejectedPromise) {
+ $adapter->promise()->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+ }
}
/** @test */
@@ -43,7 +52,11 @@ public function finallyShouldReturnAPromiseForSettledPromise(): void
$adapter = $this->getPromiseTestAdapter();
$adapter->settle(null);
- self::assertInstanceOf(PromiseInterface::class, $adapter->promise()->finally(function () {}));
+ self::assertInstanceOf(PromiseInterface::class, $promise = $adapter->promise()->finally(function () {}));
+
+ if ($promise instanceof RejectedPromise) {
+ $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+ }
}
/**
@@ -55,6 +68,10 @@ public function alwaysShouldReturnAPromiseForSettledPromise(): void
$adapter = $this->getPromiseTestAdapter();
$adapter->settle(null);
- self::assertInstanceOf(PromiseInterface::class, $adapter->promise()->always(function () {}));
+ self::assertInstanceOf(PromiseInterface::class, $promise = $adapter->promise()->always(function () {}));
+
+ if ($promise instanceof RejectedPromise) {
+ $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+ }
}
}
diff --git a/tests/PromiseTestCancelThatRejectsAfterwardsShouldNotReportUnhandled.phpt b/tests/PromiseTestCancelThatRejectsAfterwardsShouldNotReportUnhandled.phpt
new file mode 100644
index 00000000..0125a4f2
--- /dev/null
+++ b/tests/PromiseTestCancelThatRejectsAfterwardsShouldNotReportUnhandled.phpt
@@ -0,0 +1,24 @@
+--TEST--
+Calling cancel() that rejects afterwards should not report unhandled rejection
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+cancel();
+
+assert($reject instanceof \Closure);
+$reject(new \RuntimeException('Cancelled'));
+
+echo 'void' . PHP_EOL;
+
+?>
+--EXPECT--
+void
diff --git a/tests/PromiseTestCancelThatRejectsShouldNotReportUnhandled.phpt b/tests/PromiseTestCancelThatRejectsShouldNotReportUnhandled.phpt
new file mode 100644
index 00000000..fc29c646
--- /dev/null
+++ b/tests/PromiseTestCancelThatRejectsShouldNotReportUnhandled.phpt
@@ -0,0 +1,20 @@
+--TEST--
+Calling cancel() that rejects should not report unhandled rejection
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--FILE--
+cancel();
+
+echo 'void' . PHP_EOL;
+
+?>
+--EXPECT--
+void