Dart - Error Handling with Future Chain & Try Catch Block

This tutorial shows you how to catch and handle errors in Dart, which also works in Flutter.

Error Handling with Future chain

Future in Dart is described as an object that represents a delayed computation. It's used to represent a value or an error that will be available in the Future. Usually, it's used for operations that need some time to complete, such as fetching data over a network or reading from a file. Those operations are better to be performed asynchronously and usually wrapped in a function that returns Future, since you can put asynchronous operations inside a function that returns Future. Dart supports both Future chain and async/await patterns.

While the function is being executed, it may throw an error. You may need to catch the error and determine what to do if an error occurs. Below are the examples of how to handle errors in a Future chain. For this tutorial, we are going to use the below exception and function.

  class MyException implements Exception {}

  Future<String> myErrorFunction() async {
    return Future.error(new MyException(), StackTrace.current);
  }

In the code above, the function throws MyException using Future.error, with the stack trace is also passed.

Using then's onError

If you are already familiar with Dart's Future, you should have the then method. It allows you to pass a callback that will be called when the Future completes. If you look at the signature of then, there is an optional parameter onError. The callback passed as the onError argument will be called when the Future completes with an error.

The onError callback must accept one or two parameters. If it accepts one parameter, it will be called with the error. If it accepts two parameters, it will be called with the error and the stack trace. The callback needs to return a value or a Future.

  Future<R> then<R>(FutureOr<R> onValue(T value), {Function? onError});

In the code below, myErrorFunction throws MyException. The error will be caught by the onError callback. Inside the callback, you can get the details about the error and the stack trace. You can also set what value to return inside the callback.

  myErrorFunction()
    .then(
      (value) => print('Value: $value'),
      onError: (Object e, StackTrace stackTrace) {
        print(e.toString());
        print(stackTrace);
        return 'Another value';
      },
    )
    .then(print);

Output:

  Instance of 'MyException'
  #0      myErrorFunction (file:///home/ivan/Projects/coba-dart/src/error.dart:9:53)
  #1      main (file:///home/ivan/Projects/coba-dart/src/error.dart:17:3)
  #2      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:283:19)
  #3      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)
  
  Another value

Using catchError

Future has a method called catchError which is used to handle errors emitted by the Future. It's the asynchronous equivalent of a catch block.

  Future<T> catchError(Function onError, {bool test(Object error)?})

You need to pass a callback that will be called when the Future emits an error. Like the onError callback function of then in the previous example, the passed callback can have one or two parameters. When the callback is called, the error is passed as the first argument. If the callback accepts two parameters, the stack trace will be passed as the second argument. Below is an example without the test argument.

  myErrorFunction()
    .catchError((Object e, StackTrace stackTrace) {
      print(e.toString());
      print(stackTrace);
      return 'Another value';
    })
    .then(print);

The output of the code above should be the same as the previous example's output.

As you can see on the signature, it also accepts an optional argument test. For that argument, you can pass a function that accepts the error as the parameter and returns a bool. If the test argument is passed and the callback evaluates to true, the onError callback (the callback passed as the first argument) will be called. Otherwise, if the test callback evaluates to false, the onError callback will not be called and the returned Future completes with the same error and stack trace. If the test argument is not passed, it defaults to a method that returns true. Below is another example in which the test argument is passed.

  myErrorFunction()
    .catchError(
      (Object e, StackTrace stackTrace) {
        print(e.toString());
        print(stackTrace);
        return 'Another value';
      },
      test: (Object error) => error is MyException
    )
    .then(print);

The output of the code above should be the same as the previous example's output.

Using onError

Future also has another method called onError. It can be used to handle errors thrown by the Future.

  Future<T> onError<E extends Object>(
      FutureOr<T> handleError(E error, StackTrace stackTrace),
      {bool test(E error)?})

The value you need to pass as the first argument is similar to the previous examples, a callback function accepting one or two parameters. The difference is the callback function needs to return a value whose type is the same as the return type of the previous Future in the chain. Like catchError, it accepts optional test argument which is used to handle whether the passed callback should handle the emitted error or not. But you can also specify a specific error type to be caught by passing a generic type (e.g. .onError<MyException>). All errors with a different error type will not be handled.

  myErrorFunction()
    .onError<MyException>(
      (Object e, StackTrace stackTrace) {
        print(e.toString());
        print(stackTrace);

        return 'Another value';
      },
      test: (Object error) => error is MyException
    );

The output of the code above should be the same as the previous example's output.

Using whenComplete

While catchError equivalents to catch block, whenComplete is the equivalent of finally block. Therefore, if a code must be executed regardless of whether the Future completes with an error or not, you can use whenComplete.

  Future<T> whenComplete(FutureOr<void> action());

Example:

  myErrorFunction()
    .catchError(
      (Object e, StackTrace stackTrace) {
        print(e.toString());
        print(stackTrace);
      },
      test: (Object error) => error is MyException
    )
    .whenComplete(() { print('complete'); })
    .then(print);

Output:

  Instance of 'MyException'
  #0      myErrorFunction (file:///home/ivan/Projects/coba-dart/src/error.dart:9:53)
  #1      main (file:///home/ivan/Projects/coba-dart/src/error.dart:18:11)
  #2      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:283:19)
  #3      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)
  
  complete

Error Handling with Try-Catch Block

For asynchronous codes with async/await style or for non-asynchronous codes, you can use the try-catch-finally block, which is also common in other programming languages. Dart's catch accepts either one or two parameters. If an error is thrown, the error will be passed as the first argument. If the catch block accepts two parameters, the stack trace will be passed as the second argument.

  try {
    await myErrorFunction();
  } catch (e, stackTrace) {
    print(e.toString());
    print(stackTrace);
  } finally {
    print('complete');
  }

The output should be the same as the output of the previous example (whenComplete).

Summary

That's how to handle errors in Dart/Flutter. For non-asynchronous codes or for asynchronous codes with async/await style, you can use the try-catch-finally block. When using Future chain style, you can pass a callback as then's onError argument in order to handle errors. You can also use catchError and whenComplete which are the equivalents of catch and finally respectively.