Exceptions

Hierarchy

TrueAsync defines a specialized exception hierarchy for different types of errors:

\Cancellation                              -- base cancellation class (on par with \Error and \Exception)
+-- Async\AsyncCancellation                -- coroutine cancellation
    +-- Async\OperationCanceledException   -- operation interrupted by cancellation token

\Error
+-- Async\DeadlockError                    -- deadlock detected

\Exception
+-- Async\AsyncException                   -- general async operation error
|   +-- Async\ServiceUnavailableException  -- service unavailable (circuit breaker)
+-- Async\InputOutputException             -- I/O error
+-- Async\DnsException                     -- DNS resolution error
+-- Async\TimeoutException                 -- operation timeout
+-- Async\PollException                    -- poll operation error
+-- Async\ChannelException                 -- channel error
+-- Async\PoolException                    -- resource pool error
+-- Async\CompositeException               -- container for multiple exceptions

AsyncCancellation

class Async\AsyncCancellation extends \Cancellation {}

Thrown when a coroutine is cancelled. \Cancellation is the third root Throwable class on par with \Error and \Exception, so regular catch (\Exception $e) and catch (\Error $e) blocks do not accidentally catch cancellation.

<?php
use Async\AsyncCancellation;
use function Async\spawn;
use function Async\await;
use function Async\delay;

$coroutine = spawn(function() {
    try {
        delay(10000);
    } catch (AsyncCancellation $e) {
        // Gracefully finish work
        echo "Cancelled: " . $e->getMessage() . "\n";
    }
});

delay(100);
$coroutine->cancel();
?>

Important: Do not catch AsyncCancellation via catch (\Throwable $e) without re-throwing – this violates the cooperative cancellation mechanism.

OperationCanceledException

class Async\OperationCanceledException extends Async\AsyncCancellation {}

Thrown when an awaited operation is interrupted by a cancellation token. The original exception from the token is available via $previous. This allows you to distinguish a token trigger from an exception thrown by the awaitable object itself.

Affects all operations with a cancellation token: await(), await_*(), Future::await(), Channel::send()/recv(), Scope::awaitCompletion().

<?php
use Async\OperationCanceledException;
use function Async\spawn;
use function Async\await;
use function Async\timeout;
use function Async\delay;

$coroutine = spawn(function() {
    delay(10000);
    return "result";
});

try {
    await($coroutine, timeout(1000));
} catch (OperationCanceledException $e) {
    // Cancellation token triggered
    echo "Operation interrupted by token\n";
    echo "Reason: " . $e->getPrevious()?->getMessage() . "\n";
} catch (\Exception $e) {
    // Error from the coroutine itself
    echo "Error: " . $e->getMessage() . "\n";
}
?>

DeadlockError

class Async\DeadlockError extends \Error {}

Thrown when the scheduler detects a deadlock – a situation where coroutines are waiting for each other and none can proceed.

<?php
use function Async\spawn;
use function Async\await;

// Classic deadlock: two coroutines waiting for each other
$c1 = spawn(function() use (&$c2) {
    await($c2); // waits for c2
});

$c2 = spawn(function() use (&$c1) {
    await($c1); // waits for c1
});
// DeadlockError: A deadlock was detected
?>

Example where a coroutine awaits itself:

<?php
use function Async\spawn;
use function Async\await;

$coroutine = spawn(function() use (&$coroutine) {
    await($coroutine); // awaits itself
});
// DeadlockError
?>

AsyncException

class Async\AsyncException extends \Exception {}

Base exception for general async operation errors. Used for errors that don’t fall into specialized categories.

TimeoutException

class Async\TimeoutException extends \Exception {}

Thrown when a timeout is exceeded inside a coroutine. When timeout() is used as a cancellation token in await(), OperationCanceledException is thrown with TimeoutException in $previous:

<?php
use Async\OperationCanceledException;
use function Async\spawn;
use function Async\await;
use function Async\timeout;
use function Async\delay;

try {
    $coroutine = spawn(function() {
        delay(10000); // Long operation
    });
    await($coroutine, timeout(1000)); // 1 second timeout
} catch (OperationCanceledException $e) {
    // Cancellation token triggered. $e->getPrevious() is TimeoutException.
    echo "Operation didn't complete in time\n";
}
?>

InputOutputException

class Async\InputOutputException extends \Exception {}

General exception for I/O errors: sockets, files, pipes, and other I/O descriptors.

DnsException

class Async\DnsException extends \Exception {}

Thrown on DNS resolution errors (gethostbyname, gethostbyaddr, gethostbynamel).

PollException

class Async\PollException extends \Exception {}

Thrown on poll operation errors on descriptors.

ServiceUnavailableException

class Async\ServiceUnavailableException extends Async\AsyncException {}

Thrown when the circuit breaker is in the INACTIVE state and a service request is rejected without an attempt to execute.

<?php
use Async\ServiceUnavailableException;

try {
    $resource = $pool->acquire();
} catch (ServiceUnavailableException $e) {
    echo "Service is temporarily unavailable\n";
}
?>

ChannelException

class Async\ChannelException extends Async\AsyncException {}

Thrown on channel operation errors: sending to a closed channel, receiving from a closed channel, etc.

PoolException

class Async\PoolException extends Async\AsyncException {}

Thrown on resource pool operation errors.

CompositeException

final class Async\CompositeException extends \Exception
{
    public function addException(\Throwable $exception): void;
    public function getExceptions(): array;
}

A container for multiple exceptions. Used when several handlers (e.g., finally in Scope) throw exceptions during completion:

<?php
use Async\Scope;
use Async\CompositeException;

$scope = new Scope();

$scope->finally(function() {
    throw new \Exception('Cleanup error 1');
});

$scope->finally(function() {
    throw new \RuntimeException('Cleanup error 2');
});

$scope->setExceptionHandler(function($scope, $coroutine, $exception) {
    if ($exception instanceof CompositeException) {
        echo "Errors: " . count($exception->getExceptions()) . "\n";
        foreach ($exception->getExceptions() as $e) {
            echo "  - " . $e->getMessage() . "\n";
        }
    }
});

$scope->dispose();
// Errors: 2
//   - Cleanup error 1
//   - Cleanup error 2
?>

Recommendations

Properly Handling AsyncCancellation

<?php
// Correct: catch specific exceptions
try {
    await($coroutine);
} catch (\Exception $e) {
    // AsyncCancellation will NOT be caught here -- it's \Cancellation
    handleError($e);
}
<?php
// If you need to catch everything -- always re-throw AsyncCancellation
try {
    await($coroutine);
} catch (Async\AsyncCancellation $e) {
    throw $e; // Re-throw
} catch (\Throwable $e) {
    handleError($e);
}

Protecting Critical Sections

Use protect() for operations that must not be interrupted by cancellation:

<?php
use function Async\protect;

$db->beginTransaction();

protect(function() use ($db) {
    $db->exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
    $db->exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
    $db->commit();
});

See Also