Saltar al contenido principal

Futures y manejo de errores

Todo lo que querías saber sobre el manejo de errores y excepciones al escribir código asíncrono. Y algo más.

El lenguaje Dart tiene soporte nativo de asincronía, lo que hace que el código asíncrono de Dart sea mucho más fácil de leer y escribir. Sin embargo, algo de código (especialmente el código más antiguo) aún podría usar métodos de Future como then(), catchError() y whenComplete().

Esta página puede ayudarte a evitar algunas trampas comunes al usar esos métodos de Future.

La API de Future y los callbacks

#

Las funciones que usan la API de Future registran callbacks que manejan el valor (or el error) que completa un Future. Por ejemplo:

dart
myFunc().then(processValue).catchError(handleError);

Los callbacks registrados se disparan basándose en las siguientes reglas: el callback de then() se dispara si se invoca en un Future que se completa con un valor; el callback de catchError() se dispara si se invoca en un Future que se completa con un error.

En el ejemplo anterior, si el Future de myFunc() se completa con un valor, se dispara el callback de then(). Si no se genera ningún error nuevo dentro de then(), el callback de catchError() no se dispara. Por otro lado, si myFunc() se completa con un error, el callback de then() no se dispara y el de catchError() sí.

Ejemplos de uso de then() con catchError()

#

Las invocaciones encadenadas de then() y catchError() son un patrón común al trabajar con Futures, y pueden considerarse el equivalente aproximado de los bloques try-catch.

Las siguientes secciones muestran ejemplos de este patrón.

catchError() como un manejador de errores integral

#

El siguiente ejemplo trata sobre lanzar una excepción desde el callback de then() y demuestra la versatilidad de catchError() como manejador de errores:

dart
myFunc()
    .then((value) {
      doSomethingWith(value);
      ...
      throw Exception('Some arbitrary error');
    })
    .catchError(handleError);

Si el Future de myFunc() se completa con un valor, se dispara el callback de then(). Si el código dentro del callback de then() lanza una excepción (como lo hace en el ejemplo anterior), el Future de then() se completa con un error. Ese error es manejado por catchError().

Si el Future de myFunc() se completa con un error, el Future de then() se completa con ese error. El error también es manejado por catchError().

Independientemente de si el error se originó dentro de myFunc() o dentro de then(), catchError() lo maneja con éxito.

Manejo de errores dentro de then()

#

Para un manejo de errores más granular, puedes registrar un segundo callback (onError) dentro de then() para manejar los Futures completados con errores. Aquí está la firma de then():

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

Registra el callback opcional onError solo si deseas diferenciar entre un error redirigido a then() y un error generado dentro de then():

dart
asyncErrorFunction()
    .then(
      successCallback,
      onError: (e) {
        handleError(e); // Original error.
        anotherAsyncErrorFunction(); // Oops, new error.
      },
    )
    .catchError(handleError); // Error from within then() handled.

En el ejemplo anterior, el error del Future de asyncErrorFunction() se maneja con el callback onError; anotherAsyncErrorFunction() hace que el Future de then() se complete con un error; este error es manejado por catchError().

En general, no se recomienda implementar dos estrategias diferentes de manejo de errores: registra un segundo callback solo si hay una razón de peso para capturar el error dentro de then().

Errores a mitad de una cadena larga

#

Es común tener una sucesión de llamadas then() y capturar los errores generados en cualquier parte de la cadena usando catchError():

dart
Future<String> one() => Future.value('from one');
Future<String> two() => Future.error('error from two');
Future<String> three() => Future.value('from three');
Future<String> four() => Future.value('from four');

void main() {
  one() // Future completes with "from one".
      .then((_) => two()) // Future completes with two()'s error.
      .then((_) => three()) // Future completes with two()'s error.
      .then((_) => four()) // Future completes with two()'s error.
      .then((value) => value.length) // Future completes with two()'s error.
      .catchError((e) {
        print('Got error: $e'); // Finally, callback fires.
        return 42; // Future completes with 42.
      })
      .then((value) {
        print('The value is $value');
      });
}

// Output of this program:
//   Got error: error from two
//   The value is 42

En el código anterior, el Future de one() se completa con un valor, pero el Future de two() se completa con un error. Cuando se invoca then() en un Future que se completa con un error, el callback de then() no se dispara. En su lugar, el Future de then() se completa con el error de su receptor. En nuestro ejemplo, esto significa que después de que se llama a two(), el Future devuelto por cada subsubguiente then() se completa con el error de two(). Ese error finalmente se maneja dentro de catchError().

Manejo de errores específicos

#

¿Qué pasa si queremos capturar un error específico? ¿O capturar más de un error?

catchError() toma un argumento con nombre opcional, test, que nos permite consultar el tipo de error lanzado.

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

Considera handleAuthResponse(params), una función que autentica a un usuario según los parámetros proporcionados y lo redirige a una URL adecuada. Dado el complejo flujo de trabajo, handleAuthResponse() podría generar varios errores y excepciones, y deberías manejarlos de manera diferente. Así es como puedes usar test para hacer eso:

dart
void main() {
  handleAuthResponse(const {'username': 'dash', 'age': 3})
      .then((_) => ...)
      .catchError(handleFormatException, test: (e) => e is FormatException)
      .catchError(
        handleAuthorizationException,
        test: (e) => e is AuthorizationException,
      );
}

try-catch-finally asíncrono usando whenComplete()

#

Si then().catchError() refleja un try-catch, whenComplete() es el equivalente de 'finally'. El callback registrado dentro de whenComplete() se llama cuando el receptor de whenComplete() se completa, ya sea con un valor o con un error:

dart
final server = connectToServer();
server
    .post(myUrl, fields: const {'name': 'Dash', 'profession': 'mascot'})
    .then(handleResponse)
    .catchError(handleError)
    .whenComplete(server.close);

Queremos llamar a server.close independientemente de si server.post() produce una respuesta válida o un error. Nos aseguramos de que esto suceda colocándolo dentro de whenComplete().

Completado del Future devuelto por whenComplete()

#

Si no se emite ningún error desde el interior de whenComplete(), su Future se completa de la misma manera que el Future sobre el cual se invoca whenComplete(). Esto es más fácil de entender a través de ejemplos.

En el código a continuación, el Future de then() se completa con un error, por lo que el Future de whenComplete() también se completa con ese error.

dart
void main() {
  asyncErrorFunction()
      // Future completes with an error:
      .then((_) => print("Won't reach here"))
      // Future completes with the same error:
      .whenComplete(() => print('Reaches here'))
      // Future completes with the same error:
      .then((_) => print("Won't reach here"))
      // Error is handled here:
      .catchError(handleError);
}

En el código a continuación, el Future de then() se completa con un error, que ahora es manejado por catchError(). Debido a que el Future de catchError() se completa con someObject, el Future de whenComplete() se completa con ese mismo objeto.

dart
void main() {
  asyncErrorFunction()
      // Future completes with an error:
      .then((_) => ...)
      .catchError((e) {
        handleError(e);
        printErrorMessage();
        return someObject; // Future completes with someObject
      })
      .whenComplete(() => print('Done!')); // Future completes with someObject
}

Errores que se originan dentro de whenComplete()

#

Si el callback de whenComplete() lanza un error, entonces el Future de whenComplete() se completa con ese error:

dart
void main() {
  asyncErrorFunction()
      // Future completes with a value:
      .catchError(handleError)
      // Future completes with an error:
      .whenComplete(() => throw Exception('New error'))
      // Error is handled:
      .catchError(handleError);
}

Problema potencial: no registrar los manejadores de errores a tiempo

#

Es fundamental que los manejadores de errores se instalen antes de que se complete un Future: esto evita situaciones en las que un Future se completa con un error, el manejador de errores aún no está adjunto y el error se propaga accidentalmente. Considera este código:

dart
void main() {
  Future<Object> future = asyncErrorFunction();

  // BAD: Too late to handle asyncErrorFunction() exception.
  Future.delayed(const Duration(milliseconds: 500), () {
    future.then(...).catchError(...);
  });
}

En el código anterior, catchError() no se registra hasta medio segundo después de que se llama a asyncErrorFunction(), y el error queda sin manejar.

El problema desaparece si se llama a asyncErrorFunction() dentro del callback de Future.delayed():

dart
void main() {
  Future.delayed(const Duration(milliseconds: 500), () {
    asyncErrorFunction()
        .then(...)
        .catchError(...); // We get here.
  });
}

Problema potencial: mezclar accidentalmente errores síncronos y asíncronos

#

Las funciones que devuelven Futures casi siempre deberían emitir sus errores en el futuro. Dado que no queremos que quien llama a tales funciones tenga que implementar múltiples escenarios de manejo de errores, queremos evitar que se filtren errores síncronos. Considera este código:

dart
Future<int> parseAndRead(Map<String, dynamic> data) {
  final filename = obtainFilename(data); // Could throw.
  final file = File(filename);
  return file.readAsString().then((contents) {
    return parseFileData(contents); // Could throw.
  });
}

Dos funciones en ese código podrían lanzar excepciones sincrónicamente: obtainFilename() y parseFileData(). Debido a que parseFileData() se ejecuta dentro de un callback de then(), su error no se filtra fuera de la función. En su lugar, el Future de then() se completa con el error de parseFileData(), el error eventualmente completa el Future de parseAndRead() y el error puede ser manejado con éxito por catchError().

Pero obtainFilename() no se llama dentro de un callback de then(); si este lanza una excepción, se propaga un error síncrono:

dart
void main() {
  parseAndRead(data).catchError((e) {
    print('Inside catchError');
    print(e);
    return -1;
  });
}

// Program Output:
//   Unhandled exception:
//   <error from obtainFilename>
//   ...

Debido a que el uso de catchError() no captura el error, un cliente de parseAndRead() tendría que implementar una estrategia de manejo de errores separada para este error.

Solución: Usar Future.sync() para envolver tu código

#

Un patrón común para asegurar que no se lance accidentalmente ningún error síncrono desde una función es envolver el cuerpo de la función dentro de un nuevo callback de Future.sync() callback:

dart
Future<int> parseAndRead(Map<String, dynamic> data) {
  return Future.sync(() {
    final filename = obtainFilename(data); // Could throw.
    final file = File(filename);
    return file.readAsString().then((contents) {
      return parseFileData(contents); // Could throw.
    });
  });
}

Si el callback devuelve un valor que no es un Future, el Future de Future.sync() se completa con ese valor. Si el callback lanza una excepción (como sucede en el ejemplo anterior), el Future se completa con un error. Si el callback en sí devuelve un Future, el valor o el error de ese Future completa el Future de Future.sync().

Con el código envuelto dentro de Future.sync(), catchError() puede manejar todos los errores:

dart
void main() {
  parseAndRead(data).catchError((e) {
    print('Inside catchError');
    print(e);
    return -1;
  });
}

// Program Output:
//   Inside catchError
//   <error from obtainFilename>

Future.sync() hace que tu código sea resistente a excepciones no capturadas. Si tu función tiene mucho código empaquetado en ella, es probable que puedas estar haciendo algo peligroso sin darte cuenta:

dart
Future fragileFunc() {
  return Future.sync(() {
    final x = someFunc(); // Unexpectedly throws in some rare cases.
    var y = 10 / x; // x should not equal 0.
    ...
  });
}

Future.sync() no solo te permite manejar errores que sabes que podrían ocurrir, sino que también evita que los errores se filtren accidentalmente fuera de tu función.

Más información

#

Consulta la referencia de la API de Future para obtener más información sobre los Futures.