Async: Exceptions

In general, async follows this pattern:

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

However, sometimes an async function can throw an exception. The good news is that the same exception object that would be thrown in the non-async version of the code will be returned when you await the awaitable.

<?hh

namespace Hack\UserDocumentation\Async\Exceptions\Examples\BasicException;

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();
}

\HH\Asio\join(basic_exception());
Output
Fatal error: Uncaught exception 'Exception' with message 'Return exception handle' in /data/users/joelm/user-documentation/guides/hack/22-async/03-exceptions-examples/basic-exception.php:6
Stack trace:
#0 /data/users/joelm/user-documentation/guides/hack/22-async/03-exceptions-examples/basic-exception.php(14): Hack\UserDocumentation\Async\Exceptions\Examples\BasicException\exception_thrower()
#1 /data/users/joelm/user-documentation/guides/hack/22-async/03-exceptions-examples/basic-exception.php(17): Hack\UserDocumentation\Async\Exceptions\Examples\BasicException\basic_exception()
#2 {main}

Using the basic utility functions v() or m() will ignore any successful awaitable results and just throw an exception of one of the awaitable results, if one of the results was an exception.

<?hh

namespace Hack\UserDocumentation\Async\Exceptions\Examples\MultipleAwaitable;

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 = [exception_thrower(), non_exception_thrower()];
  // You will get a fatal error here with the exception thrown
  $results = await \HH\Asio\v($handles);
  // This won't happen
  var_dump($results);
}

\HH\Asio\join(multiple_waithandle_exception());
Output
Fatal error: Uncaught exception 'Exception' with message 'Return exception handle' in /data/users/joelm/user-documentation/guides/hack/22-async/03-exceptions-examples/multiple-awaitable-exception.php:6
Stack trace:
#0 /data/users/joelm/user-documentation/guides/hack/22-async/03-exceptions-examples/multiple-awaitable-exception.php(14): Hack\UserDocumentation\Async\Exceptions\Examples\MultipleAwaitable\exception_thrower()
#1 /data/users/joelm/user-documentation/guides/hack/22-async/03-exceptions-examples/multiple-awaitable-exception.php(21): Hack\UserDocumentation\Async\Exceptions\Examples\MultipleAwaitable\multiple_waithandle_exception()
#2 {main}

To get around this, and get the successful results as well, we can use the utility function called 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:

<?hh

namespace Hack\UserDocumentation\Async\Exceptions\Examples\Wrapping;

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 = [\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 \HH\Asio\v($handles);
  var_dump($results);
}

\HH\Asio\join(wrapping_exceptions());
Output
object(HH\Vector)#11 (2) {
  [0]=>
  object(HH\Asio\WrappedException)#4 (1) {
    ["exception":"HH\Asio\WrappedException":private]=>
    object(Exception)#2 (7) {
      ["message":protected]=>
      string(0) ""
      ["string":"Exception":private]=>
      string(0) ""
      ["code":protected]=>
      int(0)
      ["file":protected]=>
      string(104) "/data/users/joelm/user-documentation/guides/hack/22-async/03-exceptions-examples/wrapping-exceptions.php"
      ["line":protected]=>
      int(8)
      ["trace":"Exception":private]=>
      array(2) {
        [0]=>
        array(4) {
          ["file"]=>
          string(104) "/data/users/joelm/user-documentation/guides/hack/22-async/03-exceptions-examples/wrapping-exceptions.php"
          ["line"]=>
          int(16)
          ["function"]=>
          string(75) "Hack\UserDocumentation\Async\Exceptions\Examples\Wrapping\exception_thrower"
          ["args"]=>
          array(0) {
          }
        }
        [1]=>
        array(4) {
          ["file"]=>
          string(104) "/data/users/joelm/user-documentation/guides/hack/22-async/03-exceptions-examples/wrapping-exceptions.php"
          ["line"]=>
          int(24)
          ["function"]=>
          string(77) "Hack\UserDocumentation\Async\Exceptions\Examples\Wrapping\wrapping_exceptions"
          ["args"]=>
          array(0) {
          }
        }
      }
      ["previous":"Exception":private]=>
      NULL
    }
  }
  [1]=>
  object(HH\Asio\WrappedResult)#7 (1) {
    ["result":"HH\Asio\WrappedResult":private]=>
    int(2)
  }
}