1

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 ?

jraufeisen
  • 3,005
  • 7
  • 27
  • 43

2 Answers2

2

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());
}
jraufeisen
  • 3,005
  • 7
  • 27
  • 43
1

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 async-await with try-catch 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();
}
jamesdlin
  • 81,374
  • 13
  • 159
  • 204