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
asyncwill 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 anyExceptionthat is notDefectintoAsyncExceptionError, while also allowing anyCatchableError(includingAsyncExceptionError) to get through without compilation errors. -
New-style functions with
async: (raises: [...])annotations or their ownhandleExceptionannotations 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.