Asynchronous Operations: Exceptions

In general, an async operation has the following pattern:

  • Call an async function
  • Get an awaitable back
  • await the awaitable to get a result

If an exception is thrown within an async function body, the function does not technically throw - it returns an Awaitable that throws when resolved. This means that if an Awaitable is resolved multiple times, the same exception object instance will be thrown every time; as it is the same object every time, its data will be unchanged, including backtraces.

async function exception_thrower(): Awaitable<void> {
  throw new \Exception("Return exception handle");
}

async function basic_exception(): Awaitable<void> {
  // the handle does not throw, but result will be an Exception objection.
  // Remember, this is the same as:
  //   $handle = exception_thrower();
  //   await $handle;
  await exception_thrower();
}

<<__EntryPoint>>
function main(): void {
  \HH\Asio\join(basic_exception());
}
Output
Fatal error: Uncaught exception 'Exception' with message 'Return exception handle' in /home/example/basic-exception.hack:7
Stack trace:
#0 /home/example/basic-exception.hack(15): HHVM\UserDocumentation\Guides\Hack\AsynchronousOperations\Exceptions\BasicException\exception_thrower()
#1 /home/example/basic-exception.hack(22): HHVM\UserDocumentation\Guides\Hack\AsynchronousOperations\Exceptions\BasicException\basic_exception()
#2 (): HHVM\UserDocumentation\Guides\Hack\AsynchronousOperations\Exceptions\BasicException\main()
#3 {main}

The use of from_async ignores any successful awaitable results and just throw an exception of one of the awaitable results, if one of the results was an exception.

async function exception_thrower(): Awaitable<void> {
  throw new \Exception("Return exception handle");
}

async function non_exception_thrower(): Awaitable<int> {
  return 2;
}

async function multiple_waithandle_exception(): Awaitable<void> {
  $handles = vec[exception_thrower(), non_exception_thrower()];
  // You will get a fatal error here with the exception thrown
  $results = await Vec\from_async($handles);
  // This won't happen
  \var_dump($results);
}

<<__EntryPoint>>
function main(): void {
  \HH\Asio\join(multiple_waithandle_exception());
}
Output
Fatal error: Uncaught exception 'Exception' with message 'Return exception handle' in /home/example/multiple-awaitable-exception.hack:9
Stack trace:
#0 /home/example/multiple-awaitable-exception.hack(17): HHVM\UserDocumentation\Guides\Hack\AsynchronousOperations\Exceptions\MultipleAwaitableException\exception_thrower()
#1 /home/example/multiple-awaitable-exception.hack(28): HHVM\UserDocumentation\Guides\Hack\AsynchronousOperations\Exceptions\MultipleAwaitableException\multiple_waithandle_exception()
#2 (): HHVM\UserDocumentation\Guides\Hack\AsynchronousOperations\Exceptions\MultipleAwaitableException\main()
#3 {main}

To get around this, and get the successful results as well, we can use the utility function HH\Asio\wrap. It takes an awaitable and returns the expected result or the exception if one was thrown. The exception it gives back is of the type ResultOrExceptionWrapper.

namespace HH\Asio {
  interface ResultOrExceptionWrapper<T> {
    public function isSucceeded(): bool;
    public function isFailed(): bool;
    public function getResult(): T;
    public function getException(): \Exception;
  }
}

Taking the example above and using the wrapping mechanism, this is what the code looks like:

async function exception_thrower(): Awaitable<void> {
  throw new \Exception();
}

async function non_exception_thrower(): Awaitable<int> {
  return 2;
}

async function wrapping_exceptions(): Awaitable<void> {
  $handles = vec[
    \HH\Asio\wrap(exception_thrower()),
    \HH\Asio\wrap(non_exception_thrower()),
  ];
  // Since we wrapped, the results will contain both the exception and the
  // integer result
  $results = await Vec\from_async($handles);
  \var_dump($results);
}

<<__EntryPoint>>
function main(): void {
  \HH\Asio\join(wrapping_exceptions());
}
Output
vec(2) {
  object(HH\Asio\WrappedException) (1) {
    ["exception":"HH\Asio\WrappedException":private]=>
    object(Exception) (7) {
      ["message":protected]=>
      string(0) ""
      ["string":"Exception":private]=>
      string(0) ""
      ["code":protected]=>
      int(0)
      ["file":protected]=>
      string(145) "/home/example/wrapping-exceptions.hack"
      ["line":protected]=>
      int(9)
      ["trace":"Exception":private]=>
      varray(3) {
        darray(3) {
          ["file"]=>
          string(145) "/home/example/wrapping-exceptions.hack"
          ["line"]=>
          int(18)
          ["function"]=>
          string(105) "HHVM\UserDocumentation\Guides\Hack\AsynchronousOperations\Exceptions\WrappingExceptions\exception_thrower"
        }
        darray(3) {
          ["file"]=>
          string(145) "/home/example/wrapping-exceptions.hack"
          ["line"]=>
          int(31)
          ["function"]=>
          string(107) "HHVM\UserDocumentation\Guides\Hack\AsynchronousOperations\Exceptions\WrappingExceptions\wrapping_exceptions"
        }
        darray(1) {
          ["function"]=>
          string(92) "HHVM\UserDocumentation\Guides\Hack\AsynchronousOperations\Exceptions\WrappingExceptions\main"
        }
      }
      ["previous":"Exception":private]=>
      NULL
    }
  }
  object(HH\Asio\WrappedResult) (1) {
    ["result":"HH\Asio\WrappedResult":private]=>
    int(2)
  }
}

Memoized Async Exceptions

Because __Memoize caches the awaitable itself, if an async function is memoized and throws, you will get the same exception backtrace on every failed call.

For example, both times an exception is caught here, foo is in the backtrace, but bar is not, as the call to foo led to the creation of the memoized awaitable:

<<__Memoize>>
async function throw_something(): Awaitable<int> {
  throw new \Exception();
}

async function foo(): Awaitable<void> {
  await throw_something();
}

async function bar(): Awaitable<void> {
  await throw_something();
}

<<__EntryPoint>>
async function main(): Awaitable<void> {
  try {
    await foo();
  } catch (\Exception $e) {
    \var_dump($e->getTrace()[2] as shape('function' => string, ...)['function']);
  }
  try {
    await bar();
  } catch (\Exception $e) {
    \var_dump($e->getTrace()[2] as shape('function' => string, ...)['function']);
  }
}
Output
string(91) "HHVM\UserDocumentation\Guides\Hack\AsynchronousOperations\Exceptions\MemoizedAsyncThrow\foo"
string(91) "HHVM\UserDocumentation\Guides\Hack\AsynchronousOperations\Exceptions\MemoizedAsyncThrow\foo"