Skip to content

Commit

Permalink
Exceptions are thrown when there is no catch() function (#60)
Browse files Browse the repository at this point in the history
* wip: allow null results

* test: add failing test for #56

* feature: implement & test non-caught exceptions
closes #56

* test: reduce computational complexity
  • Loading branch information
g105b authored Jun 29, 2023
1 parent b68cd2e commit c1fbcdc
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 21 deletions.
50 changes: 33 additions & 17 deletions src/Promise.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@

class Promise implements PromiseInterface {
private mixed $resolvedValue;
private ?Throwable $rejectedReason;
private Throwable $rejectedReason;

/** @var Chainable[] */
private array $chain;
/** @var CatchChain[] */
private array $uncalledCatchChain;
/** @var callable */
private $executor;
private bool $completed;

public function __construct(callable $executor) {
$this->completed = false;
$this->chain = [];
$this->uncalledCatchChain = [];

Expand Down Expand Up @@ -90,7 +92,7 @@ function(Throwable $reason) {
$this->reject($reason);
},
function() {
$this->complete();
$this->tryComplete();
}
);
}
Expand Down Expand Up @@ -121,17 +123,15 @@ private function reset():void {
}

private function tryComplete():void {
if($this->completed) {
return;
}
if($this->getState() !== PromiseState::PENDING) {
$this->complete();
}
}

private function complete():void {
$this->sortChain();
$this->handleChain();
}

private function sortChain():void {
usort(
$this->chain,
function(Chainable $a, Chainable $b) {
Expand All @@ -145,12 +145,10 @@ function(Chainable $a, Chainable $b) {
return 0;
}
);
}

private function handleChain():void {
$handledRejections = [];

$emptyChain = empty($this->chain);
$originalChain = $this->chain;
while($this->getState() !== PromiseState::PENDING) {
$chainItem = $this->getNextChainItem();
if(!$chainItem) {
Expand All @@ -170,7 +168,9 @@ private function handleChain():void {
}
}

$this->processHandledRejections($handledRejections, $emptyChain);
$this->handleCatches($originalChain, $emptyChain, $handledRejections);

$this->completed = true;
}

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

try {
$result = $then->callOnResolved($this->resolvedValue)
?? $this->resolvedValue;
?? $this->resolvedValue ?? null;

if($result instanceof PromiseInterface) {
$this->chainPromise($result);
Expand Down Expand Up @@ -244,13 +244,29 @@ private function handleFinally(FinallyChain $finally):void {
}
}

/** @param array<Throwable> $handledRejections */
private function processHandledRejections(
/**
* @param array<Chainable> $chain
* @param bool $emptyChain
* @param array<Throwable> $handledRejections
*/
protected function handleCatches(
array $chain,
bool $emptyChain,
array $handledRejections,
bool $emptyChain
):void {
if(isset($this->rejectedReason)) {
if(!$emptyChain && !in_array($this->rejectedReason, $handledRejections)) {
if ($this->getState() === PromiseState::REJECTED) {
$hasCatch = false;
foreach ($chain as $chainItem) {
if ($chainItem instanceof CatchChain) {
$hasCatch = true;
}
}

if (!$hasCatch) {
throw $this->rejectedReason;
}

if (!$emptyChain && !in_array($this->rejectedReason, $handledRejections)) {
throw $this->rejectedReason;
}
}
Expand Down
68 changes: 64 additions & 4 deletions test/phpunit/PromiseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,10 @@ public function testGetStateFulfilled() {

public function testGetStateRejected() {
$promiseContainer = $this->getTestPromiseContainer();
$promiseContainer->reject(new Exception("Example rejection"));
$sut = $promiseContainer->getPromise();
$sut->catch(function(Throwable $throwable){});

$promiseContainer->reject(new Exception("Example rejection"));

self::assertEquals(
PromiseState::REJECTED,
Expand Down Expand Up @@ -488,12 +490,35 @@ public function testNoCatchMethodBubblesThrowables() {
$expectedException = new Exception("Test exception");
$promiseContainer = $this->getTestPromiseContainer();
$sut = $promiseContainer->getPromise();
$sut->then(function() use($expectedException) {
throw $expectedException;
});

$exception = null;
try {
$sut->then(function() use($expectedException) {
throw $expectedException;
$promiseContainer->resolve("test");
}
catch(Throwable $exception) {}

self::assertSame($expectedException, $exception);
}

public function testNoCatchMethodBubblesThrowables_internalRejection() {
$expectedException = new Exception("Test exception");
$promiseContainer = $this->getTestPromiseContainer();
$sut = $promiseContainer->getPromise();

$exception = null;
try {
$sut->then(function(string $message) use($sut, $promiseContainer, $expectedException) {
$sut->then(function($resolvedValue) use($promiseContainer, $expectedException) {
$promiseContainer->reject($expectedException);
});
return $sut;
})->catch(function(Throwable $reason) {
var_dump($reason);die("THIS IS THE REASON");
});

$promiseContainer->resolve("test");
}
catch(Throwable $exception) {}
Expand Down Expand Up @@ -641,7 +666,7 @@ public function testCustomPromise_reject() {

$customPromise->then(function($resolvedValue)use(&$resolution) {
$resolution = $resolvedValue;
}, function($rejectedValue)use(&$rejection) {
})->catch(function($rejectedValue)use(&$rejection) {
$rejection = $rejectedValue;
});

Expand Down Expand Up @@ -684,6 +709,41 @@ public function testPromise_rejectChain() {
self::assertCount(1, $catchCalls);
}

public function testPromise_notThrowWhenNoCatch():void {
$expectedException = new RuntimeException("This should be passed to the catch function");
$caughtReasons = [];

$deferred = new Deferred();
$deferredPromise = $deferred->getPromise();
$deferredPromise->then(function(string $message) use ($expectedException) {
if($message === "error") {
throw $expectedException;
}
})->catch(function(Throwable $reason) use(&$caughtReasons) {
array_push($caughtReasons, $reason);
});

$deferred->resolve("error");
self::assertCount(1, $caughtReasons);
self::assertSame($expectedException, $caughtReasons[0]);
}

public function testPromise_throwWhenNoCatch():void {
$expectedException = new RuntimeException("There was an error!");

$deferred = new Deferred();
$deferredPromise = $deferred->getPromise();
$deferredPromise->then(function(string $message) use($expectedException) {
if($message === "error") {
throw $expectedException;
}
});

self::expectException(RuntimeException::class);
self::expectExceptionMessage("There was an error!");
$deferred->resolve("error");
}

protected function getTestPromiseContainer():TestPromiseContainer {
$resolveCallback = null;
$rejectCallback = null;
Expand Down

0 comments on commit c1fbcdc

Please sign in to comment.