Dedicated exceptions for `read` failures reduce the risk of mixing up "user" exceptions with those of Future itself. The risk still exists, if the user allows a chronos exception to bubble up explicitly. Because `await` structurally guarantees that the Future is not `pending` at the time of `read`, it does not raise this new exception. * introduce `FuturePendingError` and `FutureCompletedError` when `read`:ing a future of uncertain state * fix `waitFor` / `read` to return `lent` values * simplify code generation for `void`-returning async procs * document `Raising` type helper
4.5 KiB
Errors and exceptions
Exceptions
Exceptions inheriting from CatchableError
interrupt execution of an async procedure. The exception is placed in the
Future.error field while changing the status of the Future to Failed
and callbacks are scheduled.
When a future is read or awaited the exception is re-raised, traversing the
async execution chain until handled.
proc p1() {.async.} =
await sleepAsync(1.seconds)
raise newException(ValueError, "ValueError inherits from CatchableError")
proc p2() {.async.} =
await sleepAsync(1.seconds)
proc p3() {.async.} =
let
fut1 = p1()
fut2 = p2()
await fut1
echo "unreachable code here"
await fut2
# `waitFor()` would call `Future.read()` unconditionally, which would raise the
# exception in `Future.error`.
let fut3 = p3()
while not(fut3.finished()):
poll()
echo "fut3.state = ", fut3.state # "Failed"
if fut3.failed():
echo "p3() failed: ", fut3.error.name, ": ", fut3.error.msg
# prints "p3() failed: ValueError: ValueError inherits from CatchableError"
You can put the await in a try block, to deal with that exception sooner:
proc p3() {.async.} =
let
fut1 = p1()
fut2 = p2()
try:
await fut1
except CachableError:
echo "p1() failed: ", fut1.error.name, ": ", fut1.error.msg
echo "reachable code here"
await fut2
Because chronos ensures that all exceptions are re-routed to the Future,
poll will not itself raise exceptions.
poll may still panic / raise Defect if such are raised in user code due to
undefined behavior.
Checked exceptions
By specifying a raises list to an async procedure, you can check which
exceptions can be raised by it:
proc p1(): Future[void] {.async: (raises: [IOError]).} =
assert not (compiles do: raise newException(ValueError, "uh-uh"))
raise newException(IOError, "works") # Or any child of IOError
proc p2(): Future[void] {.async, (raises: [IOError]).} =
await p1() # Works, because await knows that p1
# can only raise IOError
Under the hood, the return type of p1 will be rewritten to an internal type
which will convey raises informations to await.
Most `async` include `CancelledError` in the list of `raises`, indicating that
the operation they implement might get cancelled resulting in neither value nor
error!
When using checked exceptions, the Future type is modified to include
raises information - it can be constructed with the Raising helper:
# Create a variable of the type that will be returned by a an async function
# raising `[CancelledError]`:
var fut: Future[int].Raising([CancelledError])
`Raising` creates a specialization of `InternalRaisesFuture` type - as the name
suggests, this is an internal type whose implementation details are likely to
change in future `chronos` versions.
The Exception type
Exceptions deriving from Exception are not caught by default as these may
include Defect and other forms undefined or uncatchable behavior.
Because exception effect tracking is turned on for async functions, this may
sometimes lead to compile errors around forward declarations, methods and
closures as Nim conservatively asssumes that any Exception might be raised
from those.
Make sure to excplicitly annotate these with {.raises.}:
# Forward declarations need to explicitly include a raises list:
proc myfunction() {.raises: [ValueError].}
# ... as do `proc` types
type MyClosure = proc() {.raises: [ValueError].}
proc myfunction() =
raise (ref ValueError)(msg: "Implementation here")
let closure: MyClosure = myfunction
For compatibility, async functions can be instructed to handle Exception as
well, specifying handleException: true. Exception that is not a Defect and
not a CatchableError will then be caught and remapped to
AsyncExceptionError:
proc raiseException() {.async: (handleException: true, raises: [AsyncExceptionError]).} =
raise (ref Exception)(msg: "Raising Exception is UB")
proc callRaiseException() {.async: (raises: []).} =
try:
raiseException()
except AsyncExceptionError as exc:
# The original Exception is available from the `parent` field
echo exc.parent.msg
This mode can be enabled globally with -d:chronosHandleException as a help
when porting code to chronos but should generally be avoided as global
configuration settings may interfere with libraries that use chronos leading
to unexpected behavior.