Tips, tricks and best practices

Timeouts

To prevent a single task from taking too long, withTimeout can be used:

## Simple timeouts
import chronos

proc longTask {.async.} =
  try:
    await sleepAsync(10.minutes)
  except CancelledError as exc:
    echo "Long task was cancelled!"
    raise exc # Propagate cancellation to the next operation

proc simpleTimeout() {.async.} =
  let
    task = longTask() # Start a task but don't `await` it

  if not await task.withTimeout(1.seconds):
    echo "Timeout reached - withTimeout should have cancelled the task"
  else:
    echo "Task completed"

waitFor simpleTimeout()

When several tasks should share a single timeout, a common timer can be created with sleepAsync:

## Single timeout for several operations
import chronos

proc shortTask {.async.} =
  try:
    await sleepAsync(1.seconds)
  except CancelledError as exc:
    echo "Short task was cancelled!"
    raise exc # Propagate cancellation to the next operation

proc composedTimeout()  {.async.} =
  let
    # Common timout for several sub-tasks
    timeout = sleepAsync(10.seconds)

  while not timeout.finished():
    let task = shortTask() # Start a task but don't `await` it
    if (await race(task, timeout)) == task:
      echo "Ran one more task"
    else:
      # This cancellation may or may not happen as task might have finished
      # right at the timeout!
      task.cancelSoon()

waitFor composedTimeout()

discard

When calling an asynchronous procedure without await, the operation is started but its result is not processed until corresponding Future is read.

It is therefore important to never discard futures directly - instead, one can discard the result of awaiting the future or use asyncSpawn to monitor the outcome of the future as if it were running in a separate thread.

Similar to threads, tasks managed by asyncSpawn may causes the application to crash if any exceptions leak out of it - use checked exceptions to avoid this problem.

## The peculiarities of `discard` in `async` procedures
import chronos

proc failingOperation() {.async.} =
  echo "Raising!"
  raise (ref ValueError)(msg: "My error")

proc myApp() {.async.} =
  # This style of discard causes the `ValueError` to be discarded, hiding the
  # failure of the operation - avoid!
  discard failingOperation()

  proc runAsTask(fut: Future[void]): Future[void] {.async: (raises: []).} =
    # runAsTask uses `raises: []` to ensure at compile-time that no exceptions
    # escape it!
    try:
      await fut
    except CatchableError as exc:
      echo "The task failed! ", exc.msg

  # asyncSpawn ensures that errors don't leak unnoticed from tasks without
  # blocking:
  asyncSpawn runAsTask(failingOperation())

  # If we didn't catch the exception with `runAsTask`, the program will crash:
  asyncSpawn failingOperation()

waitFor myApp()