skip to Main Content

I came across a behaviour I am unable to work around. Basically, I am trying to fetch data, and return a Future of these data, where some of the requests can fail but we can ignore them.

This is my idea so far : for each data we want to get, create a future that returns null if it fails, and the value otherwise. Then wait all the futures, and remove null values.

Here is my code : (simple test data)

Future<List<String>> testFunc() {
  // create a result list
  List<Future<String?>> result = List.empty(growable: true);

  // put 10 future in the results
  for(int i = 0; i < 10; i++) {
    result.add(
      // create a future that may fail
      Future.delayed(Duration(seconds: i), () {
        if(i % 2 == 0) { return i; }
        else { throw Error(); }
      })
      // if the future succeed, return the value
      .then((value) {
        return "got from future : $value";
      })
      // otherwise, returns "null" as we are expecting a future returning a nullable value
      .catchError((error) {
        return null; // <========= Linter error here
      })
    );
  }

  // then, wait for all futures, and remove the ones that crashed
  return Future.wait(result).then((listOfNullable) => listOfNullable.where((element) => element != null).map((e) => e!).toList());
}

When I run this and one of the futures fails and return null, I’m having an error :
The error handler of Future.catchError must return a value of the future's type which I don’t understand, as null is a valid value for String??

Something that does work is with explicit casts :

Future<List<String>> testFunc() {
// create a result list
List<Future<String?>> result = List.empty(growable: true);

// put 10 future in the results
for(int i = 0; i < 10; i++) {
 result.add(
   // create a future that may fail
   Future.delayed(Duration(seconds: i), () {
     if(i % 2 == 0) { return i; }
     else { throw Error(); }
   })
   // if the future succeed, return the value
   .then((value) {
     return "got from future : $value" as String?; // <========= Linter error here
   })
   // otherwise, returns "null" as we are expecting a future returning a nullable value
   .catchError((error) {
     return null as String?; // <========= Linter error here
   })
 );
}

// then, wait for all futures, and remove the ones that crashed
return Future.wait(result).then((listOfNullable) => listOfNullable.where((element) => element != null).map((e) => e!).toList());
}

But now, the linter tells me I have an unnecessary cast, and I’m trying to remove any linter errors.

What I am missing here ?

2

Answers


  1. The linter errors are caused by the call to then(...), which the dart linter eagerly resolves to then<String> instead of then<String?>.

    You can specify the type explicitly to work around this behavior:

    Future<List<String>> testFunc() {
      List<Future<String?>> result = List.empty(growable: true);
    
      for(int i = 0; i < 10; i++) {
        result.add(
          Future.delayed(Duration(seconds: i), () {
            if(i % 2 == 0) { return i; }
            else { throw Error(); }
          })
          .then<String?>((value) { // <- Change here!
            return "got from future : $value";
          })
          .catchError((error) {
            return null; // No more linter warning
          })
        );
      }
    
      // then, wait for all futures, and remove the ones that crashed
      return Future.wait(result).then((listOfNullable) => listOfNullable.where((element) => element != null).map((e) => e!).toList());
    }
    
    Login or Signup to reply.
  2. The Future.catchError callback must return the same type as the original Future. That is, the callback you supply to Future<T>.catchError must return either a T or a Future<T> (also known as FutureOr<T>).

    Since you call catchError on the Future<R> returned by Future.then<R>, the error callback must return a FutureOr<R>. You do:

      .then((value) {
        return "got from future : $value"; 
      })
    

    Since you don’t provide an explicit type for Future.then<R>, R is inferred from the return type of callback, which is a non-nullable String. As jraufeisen explained, you therefore can fix this by explicitly specifying R instead of allowing it to be inferred.

    Alternatively, I strongly recommend using asyncawait with trycatch instead of using Future.then and Future.catchError. Doing so avoids these sorts of problems (or at least makes them much easier to reason about). In your case, a helper function would make that transformation easier:

    Future<List<String>> testFunc() {
      // create a result list
      List<Future<String?>> result = List.empty(growable: true);
    
      Future<String?> helper(int i) async {
        try {
          // create a future that may fail
          var value = await Future.delayed(Duration(seconds: i), () {
            if (i % 2 == 0) {
              return i;
            } else {
              throw Error();
            }
          });
          // if the future succeed, return the value
          return "got from future : $value";
        } on Error {
          // otherwise, returns "null" as we are expecting a future returning a nullable value
          return null;
        }
      }
    
      // put 10 future in the results
      for (int i = 0; i < 10; i++) {
        result.add(helper(i));
      }
    
      // then, wait for all futures, and remove the ones that crashed
      return Future.wait(result).then((listOfNullable) => listOfNullable
          .where((element) => element != null)
          .map((e) => e!)
          .toList());
    }
    

    I’d also use .whereType instead of .where with .map to filter out the nulls, use collection-for to avoid creating an empty List and growing it, and remove the remaining use of Future.then:

    Future<List<String>> testFunc() async {
      Future<String?> helper(int i) async {
        try {
          // create a future that may fail
          var value = await Future.delayed(Duration(seconds: i), () {
            if (i % 2 == 0) {
              return i;
            } else {
              throw Error();
            }
          });
          // if the future succeed, return the value
          return "got from future : $value";
        } on Error {
          // otherwise, returns "null" as we are expecting a future returning a nullable value
          return null;
        }
      }
    
      // then, wait for all futures, and remove the ones that crashed
      var result = await Future.wait([
        for (var i = 0; i < 10; i++) helper(i),
      ]);
      return result.whereType<String>().toList();
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search