Futures, async, await in Dart

Notes about asynchronous programing in Dart. In this article I test and learn about two variants of writing asynchronous code in Dart: async/await and Future-based.
Goal
As I learn Dart, I want to reinforce - or rather feel - how asynchronous programming look like. I already have some basic experience with JavaScript and I think Dart Futures are similar.
My goal in this article is to go through a very simple Dart code examples and learn about Futures, making the code a bit more complex bit by bit. I thought it would be funny to try and compare two ways (or flavours) of writing asynchronous code.
Create a future
Let’s create a simple compute
function that mimics heavy calculation. After two secons returned future would be completed with value 1.
|
|
Single optional parameter (false
by default) allows to force the function into throwing an exception MyError
. Here is the definition of MyError:
|
|
The throwing
parameter would let me test the behavior of the returned future later. I can call compute(..)
in two ways: forcing it to throw or not to force:
Complete future with value
Letthe future complete with value. main
would call compute()
and print the result:
|
|
or
|
|
Both variants do the same (print “1” after 2 seconds).
I present here both code styles which can be used to express asynchrony in Dart code. Both exist e.g. in JavaScript. If you were ever writing asynchronous code in JS then Dart will feel like home.
- first style is “callback” style in which resulting Future allows to call its method like:
then
,onError
,timeout
orcatchError
. Those methods take other functions that can handle different scenarios of possible future completion (by error or by value). - second style is called “async/await”; function that is using
await
must be declared as beingasync
in method signature.
Complete future with exception
main
would call compute({throwing: true})
and print result like this:
|
|
or like this:
|
|
Both variants produce:
|
|
Now, based on the two ways of calling asynchronous code, let’s see what we can do with exceptional situations.
Exceptions
Let’s assume our computation throws exception. What can we do about it? Let’s handle it by printing the exception to stdout. This behavior of handling an error would be implemented in a separate function and would simply print the exception:
|
|
I can use it in try-on-catch block in async main:
|
|
which will result in:
|
|
Note: this code will only catch subclasses of Exception. To handle other types, you need to catch other possible things that are thrown (everything can be thrown: integers, arbitrary objects….) (credits: SO):
|
|
Other variant of the code, using Future’s onError, requires some special attention, because this code simply does not compile:
|
|
The argument type ‘void Function(Exception)’ can’t be assigned to the parameter type ‘FutureOr
Function(Object, StackTrace)’.dartargument_type_not_assignable
so I need to:
- write a test that would filter out thrown values and only trigger handler if the test passes (here I test that thrown value is of type MyError) and
- write the handler; however, the handler is expected to actually return a FutureOr
(which is a special type denoting that I can return int
andFuture<int>
as well).
In the handler I’m returning 213, and I’m not sure how I could use this value…
Here is what I came to:
|
|
Oh. I forgot about the code in .then(....)
block! Let’s add it:
|
|
Now Dart recognized that a future in then
does not return a Future with int value; it just returns void
. Providing an int
value in onError
is compilation error. Is this really what happens? What if I’m wrong?
Let’s pretend out business case is to take computed value and then classify the value as being small or big. We have a classification function Future<Size> classify(int)
:
|
|
Using this function on then(...)
:
|
|
raises compilation error. From vscode:
|
|
Or from dart:
|
|
Fix it: provide correct the completion value
Aha! Let’s fix that by providing Size.unspecified
:
|
|
What is going on here?
Futures can be executed in succession. We are building a “pipeline” of execution. Each level of execution returns other future (possibly with other type). onError
’s type of handler function depends on what future we’re handling. Although we’re handling errors thrown anywhere up the stream, we only need to provide completion value for last future in the chain, i.e. result of classify
.
Observe the output of the above execution:
|
|
Handling exception trown in classify
If classify
threw an exception, what would be the output? I’d expect that nothing would change - actually, classify
is never called, because exception is thrown higher in the pipe, in compute. What if compute() provides int
value, and classify
fails? Let’s see:
Firs, make classify
throw on negative numbers and make compute() use provided result:
|
|
Now, let’s run the example:
- with negative
compute
value throwing: false
What would happen?compute
would return normally after 2 secondsclassify
would throw exception
So, would onError correctly report classify
exception? Let’s find out:
|
|
_handleError: My error: classify error: negative
😄 That was easy! onError handles exception thrown somewhere up the chain. Would the same be true for async variant?
|
|
The same output! Great!
Using a value in case of exception
I’m wondering: how can I use the value that onError returned to complete classify
returned future? In Future-base variant I can further chain another then(_) call and use the value with which previous Futire was completed with (i.e.Size.unspecified):
|
|
results in
|
|
and async variant requires explicit asignment in on-catch
block:
|
|
So, it seems everything can be done in both variants.
How about timout?
Timeout
Let’s say that we are presented with a business requirement to return Size.unspecified
every time the computation takes longer than 500 ms. In order to test that, let’s prepare compute()
so that it would take duration (of type Duration):
|
|
So, if we change the compute
future to timeout after requestedTimeout
, that’s all: our on Exception catch (e)
block would catch TimeoutException and correctly assign size:
|
|
Output:
|
|
With Future variant some more work is needed. Not only should we set timeout on the future, byt also note that our test function in onError
would effectively prevent handlng TimeoutException (it won’t allow _handleError
to fire as well as won’t complete the Future with Size.unspecified
).
Setting only timeout in Future variant gives this output:
|
|
Let’s relax the test, or actually, let’s remove it altogether; we could call _handleError
conditionally, but return (i.e. complete) proper Size unconditionally:
|
|
Now the above code results in proper handling of required value:
|
|
Summary
Asynchronous execution syntax, language support or at least a library support is present in almost all major languages:
Sometimes the asynchrony is single-threaded (JS) and sometimes the tasks are scheduled on a number of OS threads (go).
Different approach exist in
- Go: channels where goroutines are handled by the runtime (i.e. schedued automatically - by language runtime - for execution on syste threads)
- Java with completable futures where currently using Furure-based approach requires thinking about actual executors (and underlying thread pools). Projec Loom and the arrival of JEPP 428 would introcude structured concurrency which furter complicates/simplifies the development of concurrent/asynchronous code.
No matter what language you choose, you need to learn the API, write small programs to get used to the API and have a grasp of how to deal with exceptional situations, read, experiment and be ready for all the troubles the async execution environment brings (with non-deterministic behavior, debugging, reasoning about code flow - to name a few).
And don’t ever think you know concurrent programming. It is hard (tm) and our carbon-based hardware would always have problems in processing concurrency (both as a concept and as a code).