Las cadenas de promesas son excelentes para el manejo de errores. Cuando una promesa se rechaza, el control salta al controlador de rechazo más cercano. Esto es muy conveniente en la práctica.
Por ejemplo, en el código siguiente, la URL a fetch
es incorrecta (no existe tal sitio) y .catch
maneja el error:
fetch('https://no-such-server.blabla') // rechaza .entonces(respuesta => respuesta.json()) .catch(err => alert(err)) // TypeError: no se pudo recuperar (el texto puede variar)
Como puedes ver, el .catch
no tiene por qué ser inmediato. Puede aparecer después de uno o quizás varios .then
.
O tal vez todo está bien con el sitio, pero la respuesta no es JSON válida. La forma más sencilla de detectar todos los errores es agregar .catch
al final de la cadena:
buscar ('https://javascript.info/article/promise-chaining/user.json') .entonces(respuesta => respuesta.json()) .entonces(usuario => fetch(`https://api.github.com/users/${user.name}`)) .entonces(respuesta => respuesta.json()) .then(githubUser => nueva Promesa((resolver, rechazar) => { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promesa-avatar-ejemplo"; documento.cuerpo.append(img); setTimeout(() => { img.remove(); resolver (githubUser); }, 3000); })) .catch(error => alerta(error.mensaje));
Normalmente, ese tipo .catch
no se activa en absoluto. Pero si alguna de las promesas anteriores se rechaza (un problema de red o un json no válido o lo que sea), entonces la detectará.
El código de un ejecutor de promesas y de controladores de promesas tiene un " try..catch
invisible" a su alrededor. Si ocurre una excepción, se detecta y se trata como un rechazo.
Por ejemplo, este código:
nueva Promesa((resolver, rechazar) => { lanzar un nuevo error ("¡Ups!"); }).catch(alerta); // Error: ¡Vaya!
…Funciona exactamente igual que esto:
nueva Promesa((resolver, rechazar) => { rechazar (nuevo error ("¡Ups!")); }).catch(alerta); // Error: ¡Vaya!
El “ try..catch
invisible…catch” alrededor del ejecutor detecta automáticamente el error y lo convierte en una promesa rechazada.
Esto sucede no sólo en la función ejecutora, sino también en sus controladores. Si throw
dentro de un controlador .then
, eso significa una promesa rechazada, por lo que el control salta al controlador de errores más cercano.
He aquí un ejemplo:
nueva Promesa((resolver, rechazar) => { resolver("bien"); }).entonces((resultado) => { lanzar un nuevo error ("¡Ups!"); // rechaza la promesa }).catch(alerta); // Error: ¡Vaya!
Esto sucede con todos los errores, no solo con los causados por la declaración throw
. Por ejemplo, un error de programación:
nueva Promesa((resolver, rechazar) => { resolver("bien"); }).entonces((resultado) => { blabla(); // no existe tal función }).catch(alerta); // Error de referencia: blabla no está definido
El .catch
final no sólo detecta rechazos explícitos, sino también errores accidentales en los controladores anteriores.
Como ya notamos, .catch
al final de la cadena es similar a try..catch
. Podemos tener tantos controladores .then
como queramos y luego usar un único .catch
al final para manejar los errores en todos ellos.
En un try..catch
normal podemos analizar el error y tal vez volver a generarlo si no se puede manejar. Lo mismo es posible con las promesas.
Si throw
dentro .catch
, entonces el control pasa al siguiente controlador de errores más cercano. Y si manejamos el error y finalizamos normalmente, continúa con el siguiente controlador .then
exitoso más cercano.
En el siguiente ejemplo, .catch
maneja correctamente el error:
// la ejecución: capturar -> entonces nueva Promesa((resolver, rechazar) => { lanzar un nuevo error ("¡Ups!"); }).catch(función(error) { alert("El error ha sido solucionado, continúa normalmente"); }).then(() => alert("Próxima ejecución exitosa del controlador"));
Aquí el bloque .catch
finaliza normalmente. Entonces se llama al siguiente controlador .then
exitoso.
En el siguiente ejemplo vemos la otra situación con .catch
. El controlador (*)
detecta el error y simplemente no puede manejarlo (por ejemplo, solo sabe cómo manejar URIError
), por lo que lo arroja nuevamente:
// la ejecución: capturar -> capturar nueva Promesa((resolver, rechazar) => { lanzar un nuevo error ("¡Ups!"); }).catch(función(error) { // (*) si (instancia de error de URIError) { // manejarlo } demás { alert("No se puede manejar ese error"); error de lanzamiento; // arrojando este u otro error salta a la siguiente captura } }).entonces(función() { /* no se ejecuta aquí */ }).catch(error => { // (**) alert(`Se ha producido un error desconocido: ${error}`); // no devuelve nada => la ejecución va de forma normal });
La ejecución salta del primer .catch
(*)
al siguiente (**)
en la cadena.
¿Qué sucede cuando no se maneja un error? Por ejemplo, olvidamos agregar .catch
al final de la cadena, como aquí:
nueva Promesa(función() { noTalFunción(); // Error aquí (no existe tal función) }) .entonces(() => { // manejadores de promesas exitosos, uno o más }); // ¡sin .catch al final!
En caso de error, la promesa se rechaza y la ejecución debe saltar al controlador de rechazo más cercano. Pero no hay ninguno. Entonces el error se "atasca". No hay ningún código para manejarlo.
En la práctica, al igual que ocurre con los errores habituales de código no controlados, significa que algo salió terriblemente mal.
¿Qué sucede cuando ocurre un error regular y try..catch
no lo detecta? El script muere con un mensaje en la consola. Algo similar ocurre con los rechazos de promesas no gestionadas.
El motor JavaScript rastrea dichos rechazos y genera un error global en ese caso. Puedes verlo en la consola si ejecutas el ejemplo anterior.
En el navegador podemos detectar este tipo de errores mediante el evento unhandledrejection
:
window.addEventListener('rechazo no controlado', función(evento) { // el objeto de evento tiene dos propiedades especiales: alerta(evento.promesa); // [Promesa de objeto] - la promesa que generó el error alerta(evento.motivo); // Error: ¡Vaya! - el objeto de error no controlado }); nueva Promesa(función() { lanzar un nuevo error ("¡Ups!"); }); // no hay captura para manejar el error
El evento es parte del estándar HTML.
Si ocurre un error y no hay .catch
, el controlador unhandledrejection
se activa y obtiene el objeto event
con la información sobre el error, para que podamos hacer algo.
Normalmente estos errores son irrecuperables, por lo que nuestra mejor salida es informar al usuario sobre el problema y probablemente informar del incidente al servidor.
En entornos sin navegador como Node.js, existen otras formas de realizar un seguimiento de los errores no controlados.
.catch
maneja errores en promesas de todo tipo: ya sea una llamada reject()
o un error arrojado en un controlador.
.then
también detecta errores de la misma manera, si se le da el segundo argumento (que es el controlador de errores).
Deberíamos colocar .catch
exactamente en los lugares donde queremos manejar los errores y saber cómo manejarlos. El controlador debe analizar los errores (las clases de errores personalizadas ayudan) y volver a generar los desconocidos (tal vez sean errores de programación).
Está bien no utilizar .catch
en absoluto, si no hay forma de recuperarse de un error.
En cualquier caso, deberíamos tener el controlador de eventos unhandledrejection
(para navegadores y análogos para otros entornos) para rastrear errores no controlados e informar al usuario (y probablemente a nuestro servidor) sobre ellos, de modo que nuestra aplicación nunca “simplemente muera”.
¿Qué opinas? ¿Se activará el .catch
? Explica tu respuesta.
nueva Promesa(función(resolver, rechazar) { setTimeout(() => { lanzar un nuevo error ("¡Ups!"); }, 1000); }).catch(alerta);
La respuesta es: no, no lo hará :
nueva Promesa(función(resolver, rechazar) { setTimeout(() => { lanzar un nuevo error ("¡Ups!"); }, 1000); }).catch(alerta);
Como se dijo en el capítulo, hay un " try..catch
implícito" alrededor del código de función. De esta manera se manejan todos los errores sincrónicos.
Pero aquí el error no se genera mientras se ejecuta el ejecutor, sino más tarde. Entonces la promesa no puede soportarlo.