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:
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:
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():
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():
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():
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.
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:
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:
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.
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.
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:
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:
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():
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:
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:
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:
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:
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:
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.
A menos que se indique lo contrario, la documentación de este sitio refleja Dart 3.12.2. Página actualizada por última vez el 12-02-2025. Ver fuente oreportar un problema.