summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--lib/pure/asyncdispatch.nim63
-rw-r--r--lib/pure/asyncmacro.nim4
-rw-r--r--lib/upcoming/asyncdispatch.nim33
-rw-r--r--tests/async/tawaitsemantics.nim72
4 files changed, 130 insertions, 42 deletions
diff --git a/lib/pure/asyncdispatch.nim b/lib/pure/asyncdispatch.nim
index 802c40495..06c21e08d 100644
--- a/lib/pure/asyncdispatch.nim
+++ b/lib/pure/asyncdispatch.nim
@@ -105,6 +105,36 @@ export Port, SocketFlag
 ## be used to await a procedure returning a ``Future[void]``:
 ## ``await socket.send("foobar")``.
 ##
+## If an awaited future completes with an error, then ``await`` will re-raise
+## this error. To avoid this, you can use the ``yield`` keyword instead of
+## ``await``. The following section shows different ways that you can handle
+## exceptions in async procs.
+##
+## Handling Exceptions
+## ~~~~~~~~~~~~~~~~~~~
+##
+## The most reliable way to handle exceptions is to use ``yield`` on a future
+## then check the future's ``failed`` property. For example:
+##
+##   .. code-block:: Nim
+##     var future = sock.recv(100)
+##     yield future
+##     if future.failed:
+##       # Handle exception
+##
+## The ``async`` procedures also offer limited support for the try statement.
+##
+##    .. code-block:: Nim
+##      try:
+##        let data = await sock.recv(100)
+##        echo("Received ", data)
+##      except:
+##        # Handle exception
+##
+## Unfortunately the semantics of the try statement may not always be correct,
+## and occassionally the compilation may fail altogether.
+## As such it is better to use the former style when possible.
+##
 ## Discarding futures
 ## ------------------
 ##
@@ -339,17 +369,22 @@ proc `and`*[T, Y](fut1: Future[T], fut2: Future[Y]): Future[void] =
   var retFuture = newFuture[void]("asyncdispatch.`and`")
   fut1.callback =
     proc () =
-      if fut2.finished: retFuture.complete()
+      if not retFuture.finished:
+        if fut1.failed: retFuture.fail(fut1.error)
+        elif fut2.finished: retFuture.complete()
   fut2.callback =
     proc () =
-      if fut1.finished: retFuture.complete()
+      if not retFuture.finished:
+        if fut2.failed: retFuture.fail(fut2.error)
+        elif fut1.finished: retFuture.complete()
   return retFuture
 
 proc `or`*[T, Y](fut1: Future[T], fut2: Future[Y]): Future[void] =
   ## Returns a future which will complete once either ``fut1`` or ``fut2``
   ## complete.
   var retFuture = newFuture[void]("asyncdispatch.`or`")
-  proc cb() =
+  proc cb(fut: Future[T]) =
+    if fut.failed: retFuture.fail(fut.error)
     if not retFuture.finished: retFuture.complete()
   fut1.callback = cb
   fut2.callback = cb
@@ -374,10 +409,13 @@ proc all*[T](futs: varargs[Future[T]]): auto =
 
     for fut in futs:
       fut.callback = proc(f: Future[T]) =
-        inc(completedFutures)
+        if f.failed:
+          retFuture.fail(f.error)
+        elif not retFuture.finished:
+          inc(completedFutures)
 
-        if completedFutures == totalFutures:
-          retFuture.complete()
+          if completedFutures == totalFutures:
+            retFuture.complete()
 
     return retFuture
 
@@ -390,11 +428,14 @@ proc all*[T](futs: varargs[Future[T]]): auto =
     for i, fut in futs:
       proc setCallback(i: int) =
         fut.callback = proc(f: Future[T]) =
-          retValues[i] = f.read()
-          inc(completedFutures)
-
-          if completedFutures == len(retValues):
-            retFuture.complete(retValues)
+          if f.failed:
+            retFuture.fail(f.error)
+          elif not retFuture.finished:
+            retValues[i] = f.read()
+            inc(completedFutures)
+
+            if completedFutures == len(retValues):
+              retFuture.complete(retValues)
 
       setCallback(i)
 
diff --git a/lib/pure/asyncmacro.nim b/lib/pure/asyncmacro.nim
index 28e3e2a16..6b565cd3b 100644
--- a/lib/pure/asyncmacro.nim
+++ b/lib/pure/asyncmacro.nim
@@ -144,11 +144,9 @@ proc processBody(node, retFutureSym: NimNode,
   of nnkCommand, nnkCall:
     if node[0].kind == nnkIdent and node[0].ident == !"await":
       case node[1].kind
-      of nnkIdent, nnkInfix, nnkDotExpr:
+      of nnkIdent, nnkInfix, nnkDotExpr, nnkCall, nnkCommand:
         # await x
         # await x or y
-        result = newNimNode(nnkYieldStmt, node).add(node[1]) # -> yield x
-      of nnkCall, nnkCommand:
         # await foo(p, x)
         # await foo p, x
         var futureValue: NimNode
diff --git a/lib/upcoming/asyncdispatch.nim b/lib/upcoming/asyncdispatch.nim
index 689d0272c..460dfbd1a 100644
--- a/lib/upcoming/asyncdispatch.nim
+++ b/lib/upcoming/asyncdispatch.nim
@@ -339,17 +339,22 @@ proc `and`*[T, Y](fut1: Future[T], fut2: Future[Y]): Future[void] =
   var retFuture = newFuture[void]("asyncdispatch.`and`")
   fut1.callback =
     proc () =
-      if fut2.finished: retFuture.complete()
+      if not retFuture.finished:
+        if fut1.failed: retFuture.fail(fut1.error)
+        elif fut2.finished: retFuture.complete()
   fut2.callback =
     proc () =
-      if fut1.finished: retFuture.complete()
+      if not retFuture.finished:
+        if fut2.failed: retFuture.fail(fut2.error)
+        elif fut1.finished: retFuture.complete()
   return retFuture
 
 proc `or`*[T, Y](fut1: Future[T], fut2: Future[Y]): Future[void] =
   ## Returns a future which will complete once either ``fut1`` or ``fut2``
   ## complete.
   var retFuture = newFuture[void]("asyncdispatch.`or`")
-  proc cb() =
+  proc cb(fut: Future[T]) =
+    if fut.failed: retFuture.fail(fut.error)
     if not retFuture.finished: retFuture.complete()
   fut1.callback = cb
   fut2.callback = cb
@@ -374,10 +379,13 @@ proc all*[T](futs: varargs[Future[T]]): auto =
 
     for fut in futs:
       fut.callback = proc(f: Future[T]) =
-        inc(completedFutures)
+        if f.failed:
+          retFuture.fail(f.error)
+        elif not retFuture.finished:
+          inc(completedFutures)
 
-        if completedFutures == totalFutures:
-          retFuture.complete()
+          if completedFutures == totalFutures:
+            retFuture.complete()
 
     return retFuture
 
@@ -390,11 +398,14 @@ proc all*[T](futs: varargs[Future[T]]): auto =
     for i, fut in futs:
       proc setCallback(i: int) =
         fut.callback = proc(f: Future[T]) =
-          retValues[i] = f.read()
-          inc(completedFutures)
-
-          if completedFutures == len(retValues):
-            retFuture.complete(retValues)
+          if f.failed:
+            retFuture.fail(f.error)
+          elif not retFuture.finished:
+            retValues[i] = f.read()
+            inc(completedFutures)
+
+            if completedFutures == len(retValues):
+              retFuture.complete(retValues)
 
       setCallback(i)
 
diff --git a/tests/async/tawaitsemantics.nim b/tests/async/tawaitsemantics.nim
index 3e0c3903e..98fb5dfd5 100644
--- a/tests/async/tawaitsemantics.nim
+++ b/tests/async/tawaitsemantics.nim
@@ -2,17 +2,21 @@ discard """
   file: "tawaitsemantics.nim"
   exitcode: 0
   output: '''
-Error caught
-Test infix
-Test call
+Error can be caught using yield
+Infix `or` raises
+Infix `and` raises
+All() raises
+Awaiting a async procedure call raises
+Awaiting a future raises
 '''
 """
 
 import asyncdispatch
 
 # This tests the behaviour of 'await' under different circumstances.
-# For example, when awaiting Future variable and this future has failed the
-# exception shouldn't be raised as described here
+# Specifically, when an awaited future raises an exception then `await` should
+# also raise that exception by `read`'ing that future. In cases where you don't
+# want this behaviour, you can use `yield`.
 # https://github.com/nim-lang/Nim/issues/4170
 
 proc thrower(): Future[void] =
@@ -23,37 +27,71 @@ proc dummy: Future[void] =
   result = newFuture[void]()
   result.complete()
 
-proc testInfix() {.async.} =
-  # Test the infix operator semantics.
+proc testInfixOr() {.async.} =
+  # Test the infix `or` operator semantics.
   var fut = thrower()
   var fut2 = dummy()
-  await fut or fut2 # Shouldn't raise.
-  # TODO: what about: await thrower() or fut2?
+  await fut or fut2 # Should raise!
+
+proc testInfixAnd() {.async.} =
+  # Test the infix `and` operator semantics.
+  var fut = thrower()
+  var fut2 = dummy()
+  await fut and fut2 # Should raise!
+
+proc testAll() {.async.} =
+  # Test the `all` semantics.
+  var fut = thrower()
+  var fut2 = dummy()
+  await all(fut, fut2) # Should raise!
 
 proc testCall() {.async.} =
   await thrower()
 
+proc testAwaitFut() {.async.} =
+  var fut = thrower()
+  await fut # This should raise.
+
 proc tester() {.async.} =
   # Test that we can handle exceptions without 'try'
   var fut = thrower()
   doAssert fut.finished
   doAssert fut.failed
   doAssert fut.error.msg == "Test"
-  await fut # We are awaiting a 'Future', so no `read` occurs.
+  yield fut # We are yielding a 'Future', so no `read` occurs.
   doAssert fut.finished
   doAssert fut.failed
   doAssert fut.error.msg == "Test"
-  echo("Error caught")
+  echo("Error can be caught using yield")
+
+  fut = testInfixOr()
+  yield fut
+  doAssert fut.finished
+  doAssert fut.failed
+  echo("Infix `or` raises")
 
-  fut = testInfix()
-  await fut
+  fut = testInfixAnd()
+  yield fut
   doAssert fut.finished
-  doAssert(not fut.failed)
-  echo("Test infix")
+  doAssert fut.failed
+  echo("Infix `and` raises")
+
+  fut = testAll()
+  yield fut
+  doAssert fut.finished
+  doAssert fut.failed
+  echo("All() raises")
 
   fut = testCall()
-  await fut
+  yield fut
+  doAssert fut.failed
+  echo("Awaiting a async procedure call raises")
+
+  # Test that await will read the future and raise an exception.
+  fut = testAwaitFut()
+  yield fut
   doAssert fut.failed
-  echo("Test call")
+  echo("Awaiting a future raises")
+
 
 waitFor(tester())