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 explicitly 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
Compatibility modes
Individual functions. For compatibility, async
functions can be instructed
to handle Exception
as well, specifying handleException: true
. Any
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:
await raiseException()
except AsyncExceptionError as exc:
# The original Exception is available from the `parent` field
echo exc.parent.msg
Global flag. This mode can be enabled globally with
-d:chronosHandleException
as a help when porting code to chronos
. The
behavior in this case will be that:
-
old-style functions annotated with plain
async
will behave as if they had been annotated withasync: (handleException: true)
.This is functionally equivalent to
async: (handleException: true, raises: [CatchableError])
and will, as before, remap anyException
that is notDefect
intoAsyncExceptionError
, while also allowing anyCatchableError
(includingAsyncExceptionError
) to get through without compilation errors. -
New-style functions with
async: (raises: [...])
annotations or their ownhandleException
annotations will not be affected.
The rationale here is to allow one to incrementally introduce exception annotations and get compiler feedback while not requiring that every bit of legacy code is updated at once.
This should be used sparingly and with care, however, as global configuration
settings may interfere with libraries that use chronos
leading to unexpected
behavior.