This recipe shows you how to handle errors comprehensively when working with Futures. The accompanying code future_errors.dart
(inside the bin map in the future_errors
project) illustrates the different possibilities; however, this is not a real project, so it is not meant to be run as is.
When the function that returns a Future
value completes successfully (calls back) signaled in the code by then
, a callback function handleValue
is executed that receives the value returned. If an error condition took place, the callback handleError
handles it. Let's say this function is getFuture()
, with Future as the result and a return value of type T, then this becomes equivalent to the following code:
Future<T> future = getFuture(); future.then(handleValue) .catchError(handleError); handleValue(val) { // processing the value } handleError(err) { // handling the error }
The highlighted code is sometimes also written as follows, only to make the return values explicit:
future.then( (val) =>handleValue(val) ) .catchError( (err) =>handleError(err) );
When there is no return value, this can also be written as shown in the following code:
future.then( () =>nextStep() )
When the return value doesn't matter in the code, this can be written with an _
in place of that value, as shown in the following code:
future.then( (_) =>nextStep(_) )
But, in any case, we prefer to write succinct code, as follows:
future.then(nextStep)
The then
and catcherror
objects are chained as they are called, but that doesn't mean that they are both executed. Only one executes completely; compare it to the try-catch block in synchronous code. The
catcherror
object can even catch an error thrown in handleValue
.
This is quite an elegant mechanism, but what do we do when the code gets a little more complicated?
Let's see the different ways you can work with Futures in action:
firstStep() .then((_) =>secondStep()) .then((_) =>thirdStep()) .then((_) =>fourthStep()) .catchError(handleError);
Future
, wait
, as shown in the following code:List futs = [firstStep(), secondStep(), thirdStep(), fourthStep()]; Future.wait(futs) .then((_) =>processValues(_)) .catchError(handleError);
wait_error.dart
to see what happens when an error occurs in one of the steps (either by throw
or a Future.error
call):import'dart:async'; main() { Future<int> a = new Future(() { print('a'), return 1; }); Future<int> b = new Future.error('Error occured in b!'), Future<int> c = new Future(() { print('c'), return 3; }); Future<int> d = new Future(() { print('d'), return 4; }); Future.wait([a, b, c, d]).then((List<int> values) => print(values)).catchError(print); print('happy end'), }
The output is as follows:
happy end
a
c
d
Error occurred in b!
firstStep() .then((_) =>secondStep()) // more .then( steps ) .catchError(handleArgumentError, test: (e) => e is ArgumentError) .catchError(handleFormatException, test: (e) => e is FormatException) .catchError(handleRangeError, test: (e) => e is RangeError) .catchError(handleException, test: (e) => e is Exception);
whenComplete
:firstStep()
.then((_) =>secondStep())
.catchError(handleError)
.whenComplete(cleanup);
With respect to handling synchronous and asynchronous errors, let's suppose that we want to call a function mixedFunction
, with a synchronous call to synFunc
that could throw an exception and an asynchronous call to asynFunc
that could do likewise, as shown in the following code:
mixedFunction(data) { var var2 = new Var2(); var var1 = synFunc(data); // Could throw error. return var2.asynFunc().then(processResult); // Could throw error. }
If we call this function mixedFunction(data).catchError(handleError);,
then catchError
cannot catch an error thrown by synFunc
. To solve this, they call in a Future.sync
, function as shown in the following code:
mixedFunction(data) {
return new Future.sync(() {
var var1 = synFunc(data); // Could throw error.
return var1.asynFunc().then(processResult); // Could throw error.
});
}
That way, catchError
can catch both synchronous and asynchronous errors.
In variation 1, catchError
will handle all errors that occur in any of the executed steps. For variation 2, we make a list with all the steps. The Future.wait
option will do exactly as its name says: it will wait until all of the steps are completed. But they are executed in no particular order, so they can run concurrently. All of the functions are triggered without first waiting for any particular function to complete. When they are all done, their return values are collected in a list (here called val
) and can be processed. Again, catchError
handles any possible error that occurs in any of the steps.
In the case of an error, the List
value is not returned; we see that, in the example on wait_error
, happy end is first printed, then a
, c
, and d
complete, and then the error from b
is caught; if d
also throws an error, only the b
error is caught. The catchError
function doesn't know in which step the error occurred unless that is explicitly conveyed in the error.
In the same way as in the catch
block, we can also test in catchError
when a specific exception occurs using its second optional test
argument, where you test the type of the exception. This is shown in variation 3; be sure then, to test for a general exception as the last clause. This scenario will certainly be useful if a number of different exceptions can occur and we want a specific treatment for each of them.
Analogous to the optional finally
clause in a try
statement, asynchronous processing can have a whenComplete
handler as in variation 4, which always executes whether there is an error or not. Use it to clean up and close files, databases, and network connections, and so on.
Finally, in variation 5, the normal catchError
function won't work, because it can only handle exceptions arising from asynchronous code execution. Use Future.synchere
, which is able to return the result or error from both synchronous and asynchronous method calls.