Skip to content

Commit c1fbcdc

Browse files
author
Greg Bowler
authored
Exceptions are thrown when there is no catch() function (#60)
* wip: allow null results * test: add failing test for #56 * feature: implement & test non-caught exceptions closes #56 * test: reduce computational complexity
1 parent b68cd2e commit c1fbcdc

File tree

2 files changed

+97
-21
lines changed

2 files changed

+97
-21
lines changed

src/Promise.php

+33-17
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,18 @@
99

1010
class Promise implements PromiseInterface {
1111
private mixed $resolvedValue;
12-
private ?Throwable $rejectedReason;
12+
private Throwable $rejectedReason;
1313

1414
/** @var Chainable[] */
1515
private array $chain;
1616
/** @var CatchChain[] */
1717
private array $uncalledCatchChain;
1818
/** @var callable */
1919
private $executor;
20+
private bool $completed;
2021

2122
public function __construct(callable $executor) {
23+
$this->completed = false;
2224
$this->chain = [];
2325
$this->uncalledCatchChain = [];
2426

@@ -90,7 +92,7 @@ function(Throwable $reason) {
9092
$this->reject($reason);
9193
},
9294
function() {
93-
$this->complete();
95+
$this->tryComplete();
9496
}
9597
);
9698
}
@@ -121,17 +123,15 @@ private function reset():void {
121123
}
122124

123125
private function tryComplete():void {
126+
if($this->completed) {
127+
return;
128+
}
124129
if($this->getState() !== PromiseState::PENDING) {
125130
$this->complete();
126131
}
127132
}
128133

129134
private function complete():void {
130-
$this->sortChain();
131-
$this->handleChain();
132-
}
133-
134-
private function sortChain():void {
135135
usort(
136136
$this->chain,
137137
function(Chainable $a, Chainable $b) {
@@ -145,12 +145,10 @@ function(Chainable $a, Chainable $b) {
145145
return 0;
146146
}
147147
);
148-
}
149-
150-
private function handleChain():void {
151148
$handledRejections = [];
152149

153150
$emptyChain = empty($this->chain);
151+
$originalChain = $this->chain;
154152
while($this->getState() !== PromiseState::PENDING) {
155153
$chainItem = $this->getNextChainItem();
156154
if(!$chainItem) {
@@ -170,7 +168,9 @@ private function handleChain():void {
170168
}
171169
}
172170

173-
$this->processHandledRejections($handledRejections, $emptyChain);
171+
$this->handleCatches($originalChain, $emptyChain, $handledRejections);
172+
173+
$this->completed = true;
174174
}
175175

176176
private function getNextChainItem():?Chainable {
@@ -184,7 +184,7 @@ private function handleThen(ThenChain $then):void {
184184

185185
try {
186186
$result = $then->callOnResolved($this->resolvedValue)
187-
?? $this->resolvedValue;
187+
?? $this->resolvedValue ?? null;
188188

189189
if($result instanceof PromiseInterface) {
190190
$this->chainPromise($result);
@@ -244,13 +244,29 @@ private function handleFinally(FinallyChain $finally):void {
244244
}
245245
}
246246

247-
/** @param array<Throwable> $handledRejections */
248-
private function processHandledRejections(
247+
/**
248+
* @param array<Chainable> $chain
249+
* @param bool $emptyChain
250+
* @param array<Throwable> $handledRejections
251+
*/
252+
protected function handleCatches(
253+
array $chain,
254+
bool $emptyChain,
249255
array $handledRejections,
250-
bool $emptyChain
251256
):void {
252-
if(isset($this->rejectedReason)) {
253-
if(!$emptyChain && !in_array($this->rejectedReason, $handledRejections)) {
257+
if ($this->getState() === PromiseState::REJECTED) {
258+
$hasCatch = false;
259+
foreach ($chain as $chainItem) {
260+
if ($chainItem instanceof CatchChain) {
261+
$hasCatch = true;
262+
}
263+
}
264+
265+
if (!$hasCatch) {
266+
throw $this->rejectedReason;
267+
}
268+
269+
if (!$emptyChain && !in_array($this->rejectedReason, $handledRejections)) {
254270
throw $this->rejectedReason;
255271
}
256272
}

test/phpunit/PromiseTest.php

+64-4
Original file line numberDiff line numberDiff line change
@@ -445,8 +445,10 @@ public function testGetStateFulfilled() {
445445

446446
public function testGetStateRejected() {
447447
$promiseContainer = $this->getTestPromiseContainer();
448-
$promiseContainer->reject(new Exception("Example rejection"));
449448
$sut = $promiseContainer->getPromise();
449+
$sut->catch(function(Throwable $throwable){});
450+
451+
$promiseContainer->reject(new Exception("Example rejection"));
450452

451453
self::assertEquals(
452454
PromiseState::REJECTED,
@@ -488,12 +490,35 @@ public function testNoCatchMethodBubblesThrowables() {
488490
$expectedException = new Exception("Test exception");
489491
$promiseContainer = $this->getTestPromiseContainer();
490492
$sut = $promiseContainer->getPromise();
493+
$sut->then(function() use($expectedException) {
494+
throw $expectedException;
495+
});
491496

492497
$exception = null;
493498
try {
494-
$sut->then(function() use($expectedException) {
495-
throw $expectedException;
499+
$promiseContainer->resolve("test");
500+
}
501+
catch(Throwable $exception) {}
502+
503+
self::assertSame($expectedException, $exception);
504+
}
505+
506+
public function testNoCatchMethodBubblesThrowables_internalRejection() {
507+
$expectedException = new Exception("Test exception");
508+
$promiseContainer = $this->getTestPromiseContainer();
509+
$sut = $promiseContainer->getPromise();
510+
511+
$exception = null;
512+
try {
513+
$sut->then(function(string $message) use($sut, $promiseContainer, $expectedException) {
514+
$sut->then(function($resolvedValue) use($promiseContainer, $expectedException) {
515+
$promiseContainer->reject($expectedException);
516+
});
517+
return $sut;
518+
})->catch(function(Throwable $reason) {
519+
var_dump($reason);die("THIS IS THE REASON");
496520
});
521+
497522
$promiseContainer->resolve("test");
498523
}
499524
catch(Throwable $exception) {}
@@ -641,7 +666,7 @@ public function testCustomPromise_reject() {
641666

642667
$customPromise->then(function($resolvedValue)use(&$resolution) {
643668
$resolution = $resolvedValue;
644-
}, function($rejectedValue)use(&$rejection) {
669+
})->catch(function($rejectedValue)use(&$rejection) {
645670
$rejection = $rejectedValue;
646671
});
647672

@@ -684,6 +709,41 @@ public function testPromise_rejectChain() {
684709
self::assertCount(1, $catchCalls);
685710
}
686711

712+
public function testPromise_notThrowWhenNoCatch():void {
713+
$expectedException = new RuntimeException("This should be passed to the catch function");
714+
$caughtReasons = [];
715+
716+
$deferred = new Deferred();
717+
$deferredPromise = $deferred->getPromise();
718+
$deferredPromise->then(function(string $message) use ($expectedException) {
719+
if($message === "error") {
720+
throw $expectedException;
721+
}
722+
})->catch(function(Throwable $reason) use(&$caughtReasons) {
723+
array_push($caughtReasons, $reason);
724+
});
725+
726+
$deferred->resolve("error");
727+
self::assertCount(1, $caughtReasons);
728+
self::assertSame($expectedException, $caughtReasons[0]);
729+
}
730+
731+
public function testPromise_throwWhenNoCatch():void {
732+
$expectedException = new RuntimeException("There was an error!");
733+
734+
$deferred = new Deferred();
735+
$deferredPromise = $deferred->getPromise();
736+
$deferredPromise->then(function(string $message) use($expectedException) {
737+
if($message === "error") {
738+
throw $expectedException;
739+
}
740+
});
741+
742+
self::expectException(RuntimeException::class);
743+
self::expectExceptionMessage("There was an error!");
744+
$deferred->resolve("error");
745+
}
746+
687747
protected function getTestPromiseContainer():TestPromiseContainer {
688748
$resolveCallback = null;
689749
$rejectCallback = null;

0 commit comments

Comments
 (0)