Skip to content

Commit a025841

Browse files
committed
Trigger E_USER_ERROR on unhandled exceptions
1 parent 57d86e5 commit a025841

File tree

9 files changed

+100
-15
lines changed

9 files changed

+100
-15
lines changed

src/Internal/RejectedPromise.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,54 @@
1515
final class RejectedPromise implements PromiseInterface
1616
{
1717
private $reason;
18+
private $handled = false;
1819

1920
public function __construct(\Throwable $reason)
2021
{
2122
$this->reason = $reason;
2223
}
2324

25+
public function __destruct()
26+
{
27+
if ($this->handled) {
28+
return;
29+
}
30+
31+
$message = 'Unhandled promise rejection with ';
32+
33+
if ($this->reason instanceof \Throwable || $this->reason instanceof \Exception) {
34+
$message .= get_class($this->reason) . ': ' . $this->reason->getMessage();
35+
$message .= ' raised in ' . $this->reason->getFile() . ' on line ' . $this->reason->getLine();
36+
$message .= PHP_EOL . $this->reason->getTraceAsString();
37+
} else {
38+
if ($this->reason === null) {
39+
$message .= 'null';
40+
} else {
41+
$message .= (is_object($this->reason) ? get_class($this->reason) : gettype($this->reason));
42+
}
43+
44+
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
45+
if (isset($trace[0]['file'], $trace[0]['line'])) {
46+
$message .= ' detected in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'];
47+
}
48+
49+
ob_start();
50+
debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
51+
$message .= PHP_EOL . ob_get_clean();
52+
}
53+
54+
$message .= PHP_EOL;
55+
fatalError($message);
56+
}
57+
2458
public function then(callable $onFulfilled = null, callable $onRejected = null): PromiseInterface
2559
{
2660
if (null === $onRejected) {
2761
return $this;
2862
}
2963

64+
$this->handled = true;
65+
3066
return new Promise(function (callable $resolve, callable $reject) use ($onRejected): void {
3167
enqueue(function () use ($resolve, $reject, $onRejected): void {
3268
try {
@@ -41,6 +77,8 @@ public function then(callable $onFulfilled = null, callable $onRejected = null):
4177
public function done(callable $onFulfilled = null, callable $onRejected = null): void
4278
{
4379
enqueue(function () use ($onRejected) {
80+
$this->handled = true;
81+
4482
if (null === $onRejected) {
4583
return fatalError($this->reason);
4684
}

tests/DeferredTest.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithEx
2727
$deferred = new Deferred(function ($resolve, $reject) {
2828
$reject(new \Exception('foo'));
2929
});
30+
$deferred->promise()->then(null, function () { });
3031
$deferred->promise()->cancel();
3132
unset($deferred);
3233

@@ -42,7 +43,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejects
4243
$deferred = new Deferred(function ($resolve, $reject) {
4344
$reject(new \Exception('foo'));
4445
});
45-
$deferred->promise()->then()->cancel();
46+
$deferred->promise()->then(null, function () { })->cancel();
4647
unset($deferred);
4748

4849
$this->assertSame(0, gc_collect_cycles());
@@ -56,6 +57,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenc
5657

5758
$deferred = new Deferred(function () use (&$deferred) { });
5859
$deferred->reject(new \Exception('foo'));
60+
$deferred->promise()->then(null, function () { });
5961
unset($deferred);
6062

6163
$this->assertSame(0, gc_collect_cycles());

tests/FunctionRaceTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,6 @@ public function shouldNotCancelOtherPendingInputArrayPromisesIfOnePromiseRejects
118118

119119
$promise2 = new Promise(function () {}, $this->expectCallableNever());
120120

121-
race([$deferred->promise(), $promise2])->cancel();
121+
race([$deferred->promise(), $promise2])->then(null, function () { })->cancel();
122122
}
123123
}

tests/FunctionSomeTest.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ public function shouldNotCancelOtherPendingInputArrayPromisesIfEnoughPromisesRej
163163

164164
$promise2 = new Promise(function () {}, $this->expectCallableNever());
165165

166-
some([$deferred->promise(), $promise2], 2);
166+
$ret = some([$deferred->promise(), $promise2], 2);
167+
168+
$ret->then(null, function () { });
167169
}
168170
}

tests/Internal/RejectedPromiseTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Exception;
66
use LogicException;
7+
use React\Promise\ErrorCollector;
78
use React\Promise\PromiseAdapter\CallbackPromiseAdapter;
89
use React\Promise\PromiseTest\PromiseRejectedTestTrait;
910
use React\Promise\PromiseTest\PromiseSettledTestTrait;
@@ -45,4 +46,19 @@ public function getPromiseTestAdapter(callable $canceller = null)
4546
},
4647
]);
4748
}
49+
50+
/** @test */
51+
public function unhandledRejectionShouldTriggerFatalError()
52+
{
53+
$errorCollector = new ErrorCollector();
54+
$errorCollector->start();
55+
56+
$promise = new RejectedPromise(new Exception('foo'));
57+
unset($promise);
58+
59+
$errors = $errorCollector->stop();
60+
61+
self::assertEquals(E_USER_ERROR, $errors[0]['errno']);
62+
self::assertContains('Unhandled promise rejection with Exception: foo raised in ', $errors[0]['errstr']);
63+
}
4864
}

tests/PromiseTest.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptio
6666
$promise = new Promise(function () {
6767
throw new \Exception('foo');
6868
});
69+
$promise->then(null, function () { });
6970
unset($promise);
7071

7172
$this->assertSame(0, gc_collect_cycles());
@@ -78,6 +79,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverRejectsWithExc
7879
$promise = new Promise(function ($resolve, $reject) {
7980
$reject(new \Exception('foo'));
8081
});
82+
$promise->then(null, function () { });
8183
unset($promise);
8284

8385
$this->assertSame(0, gc_collect_cycles());
@@ -91,6 +93,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithEx
9193
$reject(new \Exception('foo'));
9294
});
9395
$promise->cancel();
96+
$promise->then(null, function () { });
9497
unset($promise);
9598

9699
$this->assertSame(0, gc_collect_cycles());
@@ -103,7 +106,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejects
103106
$promise = new Promise(function ($resolve, $reject) { }, function ($resolve, $reject) {
104107
$reject(new \Exception('foo'));
105108
});
106-
$promise->then()->then()->then()->cancel();
109+
$promise->then()->then()->then(null, function () { })->cancel();
107110
unset($promise);
108111

109112
$this->assertSame(0, gc_collect_cycles());
@@ -116,6 +119,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptio
116119
$promise = new Promise(function ($resolve, $reject) {
117120
throw new \Exception('foo');
118121
});
122+
$promise->then(null, function () { });
119123
unset($promise);
120124

121125
$this->assertSame(0, gc_collect_cycles());
@@ -141,6 +145,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReference
141145
throw new \Exception('foo');
142146
});
143147
$promise->cancel();
148+
$promise->then(null, function () { });
144149
unset($promise);
145150

146151
$this->assertSame(0, gc_collect_cycles());
@@ -157,6 +162,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverWithReferenceT
157162
$promise = new Promise(function () use (&$promise) {
158163
throw new \Exception('foo');
159164
});
165+
$promise->then(null, function () { });
160166
unset($promise);
161167

162168
$this->assertSame(0, gc_collect_cycles());
@@ -173,6 +179,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenc
173179
$promise = new Promise(function () {
174180
throw new \Exception('foo');
175181
}, function () use (&$promise) { });
182+
$promise->then(null, function () { });
176183
unset($promise);
177184

178185
$this->assertSame(0, gc_collect_cycles());
@@ -186,7 +193,7 @@ public function shouldIgnoreNotifyAfterReject()
186193
$notify(42);
187194
});
188195

189-
$promise->then(null, null, $this->expectCallableNever());
196+
$promise->then(null, function () { }, $this->expectCallableNever());
190197
$promise->cancel();
191198
}
192199

@@ -263,6 +270,7 @@ public function shouldFulfillIfFullfilledWithSimplePromise()
263270
$promise = new Promise(function () {
264271
throw new Exception('foo');
265272
});
273+
$promise->then(null, function () { });
266274
unset($promise);
267275

268276
self::assertSame(0, gc_collect_cycles());

tests/PromiseTest/CancelTestTrait.php

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,15 +105,13 @@ public function cancelShouldRejectPromiseWithExceptionIfCancellerThrows()
105105
/** @test */
106106
public function cancelShouldCallCancellerOnlyOnceIfCancellerResolves()
107107
{
108-
$mock = $this->createCallableMock();
109-
$mock
110-
->expects($this->once())
111-
->method('__invoke')
112-
->will($this->returnCallback(function ($resolve) {
113-
$resolve();
114-
}));
108+
$once = $this->expectCallableOnce();
109+
$canceller = function ($resolve) use ($once) {
110+
$resolve();
111+
$once();
112+
};
115113

116-
$adapter = $this->getPromiseTestAdapter($mock);
114+
$adapter = $this->getPromiseTestAdapter($canceller);
117115

118116
$adapter->promise()->cancel();
119117
$adapter->promise()->cancel();

tests/PromiseTest/PromiseRejectedTestTrait.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,10 +378,13 @@ public function otherwiseShouldNotInvokeRejectionHandlerIfReaonsDoesNotMatchType
378378
$mock = $this->expectCallableNever();
379379

380380
$adapter->reject($exception);
381-
$adapter->promise()
381+
$ret = $adapter->promise()
382382
->otherwise(function (InvalidArgumentException $reason) use ($mock) {
383383
$mock($reason);
384384
});
385+
386+
$ret->then(null, function () { });
387+
$adapter->promise()->then(null, function () { });
385388
}
386389

387390
/** @test */
@@ -497,6 +500,8 @@ public function cancelShouldReturnNullForRejectedPromise()
497500
$adapter->reject(new Exception());
498501

499502
self::assertNull($adapter->promise()->cancel());
503+
504+
$adapter->promise()->then(null, function () { });
500505
}
501506

502507
/** @test */
@@ -507,5 +512,7 @@ public function cancelShouldHaveNoEffectForRejectedPromise()
507512
$adapter->reject(new Exception());
508513

509514
$adapter->promise()->cancel();
515+
516+
$adapter->promise()->then(null, function () { });
510517
}
511518
}

tests/PromiseTest/PromiseSettledTestTrait.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public function thenShouldReturnAPromiseForSettledPromise()
1919

2020
$adapter->settle();
2121
self::assertInstanceOf(PromiseInterface::class, $adapter->promise()->then());
22+
23+
$adapter->promise()->then(null, function () { });
2224
}
2325

2426
/** @test */
@@ -28,6 +30,8 @@ public function thenShouldReturnAllowNullForSettledPromise()
2830

2931
$adapter->settle();
3032
self::assertInstanceOf(PromiseInterface::class, $adapter->promise()->then(null, null));
33+
34+
$adapter->promise()->then(null, function () { });
3135
}
3236

3337
/** @test */
@@ -38,6 +42,8 @@ public function cancelShouldReturnNullForSettledPromise()
3842
$adapter->settle();
3943

4044
self::assertNull($adapter->promise()->cancel());
45+
46+
$adapter->promise()->then(null, function () { });
4147
}
4248

4349
/** @test */
@@ -48,6 +54,8 @@ public function cancelShouldHaveNoEffectForSettledPromise()
4854
$adapter->settle();
4955

5056
$adapter->promise()->cancel();
57+
58+
$adapter->promise()->then(null, function () { });
5159
}
5260

5361
/** @test */
@@ -57,6 +65,8 @@ public function doneShouldReturnNullForSettledPromise()
5765

5866
$adapter->settle();
5967
self::assertNull($adapter->promise()->done(null, function () {}));
68+
69+
$adapter->promise()->then(null, function () { });
6070
}
6171

6272
/** @test */
@@ -66,6 +76,8 @@ public function doneShouldReturnAllowNullForSettledPromise()
6676

6777
$adapter->settle();
6878
self::assertNull($adapter->promise()->done(null, function () {}, null));
79+
80+
$adapter->promise()->then(null, function () { });
6981
}
7082

7183
/** @test */
@@ -74,6 +86,8 @@ public function alwaysShouldReturnAPromiseForSettledPromise()
7486
$adapter = $this->getPromiseTestAdapter();
7587

7688
$adapter->settle();
77-
self::assertInstanceOf(PromiseInterface::class, $adapter->promise()->always(function () {}));
89+
self::assertInstanceOf(PromiseInterface::class, $ret = $adapter->promise()->always(function () {}));
90+
91+
$ret->then(null, function () { });
7892
}
7993
}

0 commit comments

Comments
 (0)