No importa lo buenos que seamos programando, a veces nuestros scripts tienen errores. Pueden ocurrir debido a errores nuestros, una entrada inesperada del usuario, una respuesta errónea del servidor y por mil razones más.
Por lo general, un script "muere" (se detiene inmediatamente) en caso de error, imprimiéndolo en la consola.
Pero hay una construcción de sintaxis try...catch
que nos permite "detectar" errores para que el script pueda, en lugar de morir, hacer algo más razonable.
La construcción try...catch
tiene dos bloques principales: try
y luego catch
:
intentar { // código... } atrapar (errar) { // manejo de errores }
Funciona así:
Primero, se ejecuta el código en try {...}
.
Si no hubo errores, entonces se ignora catch (err)
: la ejecución llega al final del try
y continúa, omitiendo catch
.
Si ocurre un error, entonces se detiene la ejecución try
y el control fluye al comienzo de catch (err)
. La variable err
(podemos usar cualquier nombre) contendrá un objeto de error con detalles sobre lo que sucedió.
Por lo tanto, un error dentro del bloque try {...}
no mata el script; tenemos la posibilidad de manejarlo en catch
.
Veamos algunos ejemplos.
Un ejemplo sin errores: muestra alert
(1)
y (2)
:
intentar { alert('Inicio de las ejecuciones de prueba'); // (1) <-- // ...no hay errores aquí alert('Fin de las ejecuciones de intento'); // (2) <-- } atrapar (errar) { alert('Se ignora la captura porque no hay errores'); // (3) }
Un ejemplo con un error: muestra (1)
y (3)
:
intentar { alert('Inicio de las ejecuciones de prueba'); // (1) <-- lalala; // error, ¡la variable no está definida! alert('Fin del intento (nunca alcanzado)'); // (2) } atrapar (errar) { alert(`¡Se ha producido un error!`); // (3) <-- }
try...catch
sólo funciona para errores de tiempo de ejecución
Para que try...catch
funcione, el código debe poder ejecutarse. En otras palabras, debería ser JavaScript válido.
No funcionará si el código es sintácticamente incorrecto, por ejemplo, tiene llaves no coincidentes:
intentar { {{{{{{{{{{{{ } atrapar (errar) { alert("El motor no puede entender este código, no es válido"); }
El motor JavaScript primero lee el código y luego lo ejecuta. Los errores que ocurren en la fase de lectura se denominan errores de "tiempo de análisis" y son irrecuperables (desde dentro de ese código). Eso es porque el motor no puede entender el código.
Entonces, try...catch
solo puede manejar errores que ocurren en código válido. Estos errores se denominan "errores de ejecución" o, a veces, "excepciones".
try...catch
funciona sincrónicamente
Si ocurre una excepción en el código "programado", como en setTimeout
, try...catch
no la detectará:
intentar { setTimeout(función() { noTalVariable; // el script morirá aquí }, 1000); } atrapar (errar) { alerta("no funcionará"); }
Esto se debe a que la función en sí se ejecuta más tarde, cuando el motor ya abandonó la construcción try...catch
.
Para detectar una excepción dentro de una función programada, try...catch
debe estar dentro de esa función:
setTimeout(función() { intentar { noTalVariable; // try...catch maneja el error! } atrapar { alert("¡Se detectó un error aquí!"); } }, 1000);
Cuando ocurre un error, JavaScript genera un objeto que contiene los detalles al respecto. Luego, el objeto se pasa como argumento para catch
:
intentar { //... } catch (err) { // <-- el "objeto de error", podría usar otra palabra en lugar de err //... }
Para todos los errores integrados, el objeto de error tiene dos propiedades principales:
name
Nombre del error. Por ejemplo, para una variable indefinida que es "ReferenceError"
.
message
Mensaje de texto sobre detalles del error.
Hay otras propiedades no estándar disponibles en la mayoría de los entornos. Uno de los más utilizados y soportados es:
stack
Pila de llamadas actual: una cadena con información sobre la secuencia de llamadas anidadas que provocaron el error. Se utiliza con fines de depuración.
Por ejemplo:
intentar { lalala; // error, ¡la variable no está definida! } atrapar (errar) { alerta(error.nombre); // Error de referencia alerta (mensaje de error); // lalala no está definida alerta(err.pila); // ReferenceError: lalala no está definida en (...pila de llamadas) // También puede mostrar un error completo // El error se convierte en una cadena como "nombre: mensaje" alerta(errar); // Error de referencia: lalala no está definida }
Una adición reciente
Esta es una adición reciente al idioma. Los navegadores antiguos pueden necesitar polyfills.
Si no necesitamos detalles del error, catch
puede omitirlo:
intentar { //... } captura { // <-- sin (err) //... }
Exploremos un caso de uso de la vida real de try...catch
.
Como ya sabemos, JavaScript admite el método JSON.parse(str) para leer valores codificados en JSON.
Generalmente se utiliza para decodificar datos recibidos a través de la red, desde el servidor u otra fuente.
Lo recibimos y llamamos a JSON.parse
así:
let json = '{"nombre":"Juan", "edad": 30}'; //datos del servidor dejar usuario = JSON.parse(json); // convierte la representación de texto en un objeto JS // ahora el usuario es un objeto con propiedades de la cadena alerta (nombre.usuario); // John alerta (usuario.edad); // 30
Puede encontrar información más detallada sobre JSON en los métodos JSON, en el capítulo JSON.
Si json
tiene un formato incorrecto, JSON.parse
genera un error, por lo que el script "muere".
¿Deberíamos estar satisfechos con eso? ¡Por supuesto que no!
De esta manera, si hay algún problema con los datos, el visitante nunca lo sabrá (a menos que abra la consola del desarrollador). Y a la gente realmente no le gusta cuando algo “simplemente muere” sin ningún mensaje de error.
Usemos try...catch
para manejar el error:
let json = "{mal json}"; intentar { dejar usuario = JSON.parse(json); // <-- cuando ocurre un error... alerta (nombre.usuario); // no funciona } atrapar (errar) { // ...la ejecución salta aquí alert("Nuestras disculpas, los datos tienen errores, intentaremos solicitarlos una vez más." ); alerta( error.nombre ); alerta (err.mensaje); }
Aquí usamos el bloque catch
solo para mostrar el mensaje, pero podemos hacer mucho más: enviar una nueva solicitud de red, sugerir una alternativa al visitante, enviar información sobre el error a una instalación de registro,…. Todo mucho mejor que simplemente morir.
¿Qué pasa si json
es sintácticamente correcto, pero no tiene una propiedad name
requerida?
Como esto:
let json = '{ "edad": 30 }'; //datos incompletos intentar { dejar usuario = JSON.parse(json); // <-- sin errores alerta (nombre.usuario); // ¡sin nombre! } atrapar (errar) { alerta("no se ejecuta"); }
Aquí JSON.parse
se ejecuta normalmente, pero la ausencia del name
es en realidad un error para nosotros.
Para unificar el manejo de errores, usaremos el operador throw
.
El operador throw
genera un error.
La sintaxis es:
tirar <objeto de error>
Técnicamente, podemos usar cualquier cosa como objeto de error. Puede ser incluso una primitiva, como un número o una cadena, pero es mejor usar objetos, preferiblemente con propiedades name
y message
(para seguir siendo algo compatible con los errores integrados).
JavaScript tiene muchos constructores integrados para errores estándar: Error
, SyntaxError
, ReferenceError
, TypeError
y otros. También podemos usarlos para crear objetos de error.
Su sintaxis es:
let error = nuevo Error(mensaje); // o let error = nuevo SyntaxError(mensaje); let error = new ReferenceError(mensaje); //...
Para errores integrados (no para ningún objeto, solo para errores), la propiedad name
es exactamente el nombre del constructor. Y message
se toma del argumento.
Por ejemplo:
let error = new Error("Suceden cosas o_O"); alerta(error.nombre); // Error alerta(error.mensaje); // Pasan cosas o_O
Veamos qué tipo de error genera JSON.parse
:
intentar { JSON.parse("{mal json o_O}"); } atrapar (errar) { alerta(error.nombre); // Error de sintaxis alerta (mensaje de error); // Token b inesperado en JSON en la posición 2 }
Como podemos ver, eso es un SyntaxError
.
Y en nuestro caso la ausencia de name
es un error, ya que los usuarios deben tener un name
.
Así que vamos a tirarlo:
let json = '{ "edad": 30 }'; //datos incompletos intentar { dejar usuario = JSON.parse(json); // <-- sin errores si (!nombre.usuario) { throw new SyntaxError("Datos incompletos: sin nombre"); // (*) } alerta (nombre.usuario); } atrapar (errar) { alerta( "Error JSON: " + err.message ); // Error JSON: datos incompletos: sin nombre }
En la línea (*)
, el operador throw
genera un SyntaxError
con el message
dado, de la misma manera que JavaScript lo generaría por sí mismo. La ejecución de try
se detiene inmediatamente y el flujo de control pasa a catch
.
Ahora catch
se convirtió en un lugar único para todo el manejo de errores: tanto para JSON.parse
como para otros casos.
En el ejemplo anterior usamos try...catch
para manejar datos incorrectos. ¿Pero es posible que ocurra otro error inesperado dentro del bloque try {...}
? Como un error de programación (la variable no está definida) o algo más, no sólo este asunto de los “datos incorrectos”.
Por ejemplo:
let json = '{ "edad": 30 }'; //datos incompletos intentar { usuario = JSON.parse(json); // <-- olvidé poner "let" antes del usuario //... } atrapar (errar) { alerta("Error JSON: " + err); // Error JSON: ReferenceError: el usuario no está definido // (en realidad no hay error JSON) }
¡Por supuesto que todo es posible! Los programadores cometen errores. Incluso en las utilidades de código abierto utilizadas por millones de personas durante décadas, de repente se puede descubrir un error que conduzca a terribles ataques.
En nuestro caso, try...catch
se coloca para detectar errores de "datos incorrectos". Pero por su naturaleza, catch
obtiene todos los errores de try
. Aquí aparece un error inesperado, pero aún muestra el mismo mensaje "JSON Error"
. Eso está mal y también hace que el código sea más difícil de depurar.
Para evitar este tipo de problemas, podemos emplear la técnica del “relanzamiento”. La regla es simple:
Catch sólo debe procesar los errores que conoce y "volver a lanzar" todos los demás.
La técnica del “relanzamiento” se puede explicar con más detalle como:
Catch obtiene todos los errores.
En el bloque catch (err) {...}
analizamos el objeto de error err
.
Si no sabemos cómo manejarlo, throw err
.
Normalmente, podemos verificar el tipo de error usando el operador instanceof
:
intentar { usuario = { /*...*/ }; } atrapar (errar) { if (err instancia de error de referencia) { alerta('Error de referencia'); // "ReferenceError" para acceder a una variable no definida } }
También podemos obtener el nombre de la clase de error de la propiedad err.name
. Todos los errores nativos lo tienen. Otra opción es leer err.constructor.name
.
En el código siguiente, utilizamos el relanzamiento para que catch
solo maneje SyntaxError
:
let json = '{ "edad": 30 }'; //datos incompletos intentar { dejar usuario = JSON.parse(json); si (!nombre.usuario) { throw new SyntaxError("Datos incompletos: sin nombre"); } blabla(); // error inesperado alerta (nombre.usuario); } atrapar (errar) { if (err instancia de SyntaxError) { alerta( "Error JSON: " + err.message ); } demás { tirar errar; // volver a lanzar (*) } }
El error al lanzar en línea (*)
desde el interior del bloque catch
“se cae” de try...catch
y puede ser detectado por una construcción try...catch
externa (si existe), o mata el script.
Entonces, el bloque catch
en realidad maneja solo los errores que sabe cómo manejar y "salta" todos los demás.
El siguiente ejemplo demuestra cómo estos errores pueden detectarse mediante un nivel más de try...catch
:
función leerDatos() { let json = '{ "edad": 30 }'; intentar { //... blabla(); // ¡error! } atrapar (errar) { //... if (!(err instancia de SyntaxError)) { tirar errar; // volver a lanzar (no sé cómo solucionarlo) } } } intentar { leerDatos(); } atrapar (errar) { alert( "Se obtuvo captura externa: " + err ); // ¡lo atrapé! }
Aquí readData
solo sabe cómo manejar SyntaxError
, mientras que el try...catch
externo sabe cómo manejar todo.
Espera, eso no es todo.
La construcción try...catch
puede tener una cláusula de código más: finally
.
Si existe, se ejecuta en todos los casos:
después try
, si no hubo errores,
después de catch
, si hubo errores.
La sintaxis extendida se ve así:
intentar { ...intenta ejecutar el código... } atrapar (errar) { ... manejar errores ... } finalmente { ... ejecutar siempre ... }
Intente ejecutar este código:
intentar { alerta('intentar'); if (confirm('¿Cometió un error?')) BAD_CODE(); } atrapar (errar) { alerta('captura'); } finalmente { alerta('finalmente'); }
El código tiene dos formas de ejecución:
Si responde “Sí” a “¿Cometió un error?”, try -> catch -> finally
.
Si dice "No", try -> finally
.
La cláusula finally
se usa a menudo cuando comenzamos a hacer algo y queremos finalizarlo en cualquier caso de resultado.
Por ejemplo, queremos medir el tiempo que tarda una función de números de Fibonacci fib(n)
. Naturalmente, podemos empezar a medir antes de que se ejecute y terminar después. Pero ¿qué pasa si hay un error durante la llamada a la función? En particular, la implementación de fib(n)
en el siguiente código devuelve un error para números negativos o no enteros.
La cláusula finally
es un excelente lugar para terminar las mediciones pase lo que pase.
Aquí finally
se garantiza que el tiempo se medirá correctamente en ambas situaciones – en caso de una ejecución exitosa de fib
y en caso de un error en el mismo:
let num = +prompt("¿Ingrese un número entero positivo?", 35) dejar diferenciar, resultado; función fib(n) { si (n < 0 || Math.trunc(n) != n) { throw new Error("No debe ser negativo y también un número entero."); } devolver n <= 1? n : fib(n - 1) + fib(n - 2); } dejar empezar = Fecha.ahora(); intentar { resultado = fib(núm); } atrapar (errar) { resultado = 0; } finalmente { diff = Fecha.ahora() - inicio; } alerta(resultado || "ocurrió un error"); alert(`la ejecución tomó ${diff}ms`);
Puede comprobarlo ejecutando el código ingresando 35
en prompt
; se ejecuta normalmente, finally
después de try
. Y luego ingrese -1
; habrá un error inmediato y la ejecución tardará 0ms
. Ambas mediciones se realizan correctamente.
En otras palabras, la función puede terminar con return
o throw
, eso no importa. La cláusula finally
se ejecuta en ambos casos.
Las variables son locales dentro de try...catch...finally
Tenga en cuenta que las variables result
y diff
en el código anterior se declaran antes de try...catch
.
De lo contrario, si declaramos let
en el bloque try
, solo será visible dentro de él.
finally
y return
La cláusula finally
funciona para cualquier salida de try...catch
. Eso incluye una return
explícita.
En el siguiente ejemplo, hay un return
en try
. En este caso, finally
se ejecuta justo antes de que el control regrese al código externo.
función función() { intentar { devolver 1; } atrapar (errar) { /* ... */ } finalmente { alerta('finalmente'); } } alerta( función() ); // primero funciona la alerta de finalmente, y luego esta
try...finally
La construcción try...finally
, sin cláusula catch
, también es útil. Lo aplicamos cuando no queremos manejar errores aquí (dejarlos caer), pero queremos estar seguros de que los procesos que iniciamos estén finalizados.
función función() { // empezar a hacer algo que necesita completarse (como mediciones) intentar { //... } finalmente { // completa esa cosa incluso si todos mueren } }
En el código anterior, siempre aparece un error dentro try
, porque no hay ningún catch
. Pero finally
funciona antes de que el flujo de ejecución abandone la función.
Específico del entorno
La información de esta sección no forma parte del JavaScript principal.
Imaginemos que tenemos un error fatal fuera de try...catch
y el script falló. Como un error de programación o alguna otra cosa terrible.
¿Hay alguna manera de reaccionar ante tales sucesos? Es posible que queramos registrar el error, mostrarle algo al usuario (normalmente no ve mensajes de error), etc.
No hay ninguno en la especificación, pero los entornos suelen proporcionarlo porque es realmente útil. Por ejemplo, Node.js tiene process.on("uncaughtException")
para eso. Y en el navegador podemos asignar una función a la propiedad especial window.onerror, que se ejecutará en caso de que se produzca un error no detectado.
La sintaxis:
ventana.onerror = función (mensaje, URL, línea, columna, error) { //... };
message
Mensaje de error.
url
URL del script donde ocurrió el error.
line
, col
Números de línea y columna donde ocurrió el error.
error
Objeto de error.
Por ejemplo:
<guión> ventana.onerror = función (mensaje, URL, línea, columna, error) { alert(`${mensaje}n En ${line}:${col} de ${url}`); }; función leerDatos() { malaFunc(); // ¡Vaya, algo salió mal! } leerDatos(); </script>
La función del controlador global window.onerror
normalmente no es recuperar la ejecución del script (esto probablemente sea imposible en caso de errores de programación), sino enviar el mensaje de error a los desarrolladores.
También existen servicios web que proporcionan registro de errores para estos casos, como https://errorception.com o https://www.muscula.com.
Funcionan así:
Nos registramos en el servicio y obtenemos un fragmento de JS (o una URL de script) para insertarlo en las páginas.
Ese script JS establece una función window.onerror
personalizada.
Cuando ocurre un error, envía una solicitud de red al servicio.
Podemos iniciar sesión en la interfaz web del servicio y ver errores.
La construcción try...catch
permite manejar errores de tiempo de ejecución. Literalmente permite “intentar” ejecutar el código y “detectar” los errores que puedan ocurrir en el mismo.
La sintaxis es:
intentar { // ejecuta este código } atrapar (errar) { // si ocurrió un error, entonces salta aquí // err es el objeto del error } finalmente { // hacerlo en cualquier caso después de try/catch }
Puede que no haya una sección catch
o que no haya finally
, por lo que las construcciones más cortas try...catch
y try...finally
también son válidas.
Los objetos de error tienen las siguientes propiedades:
message
: el mensaje de error legible por humanos.
name
: la cadena con el nombre del error (nombre del constructor del error).
stack
(no estándar, pero con buen soporte): la pila en el momento de la creación del error.
Si no se necesita un objeto de error, podemos omitirlo usando catch {
en lugar de catch (err) {
.
También podemos generar nuestros propios errores usando el operador throw
. Técnicamente, el argumento de throw
puede ser cualquier cosa, pero generalmente es un objeto de error que hereda de la clase Error
incorporada. Más información sobre errores de extensión en el próximo capítulo.
Volver a lanzar es un patrón muy importante de manejo de errores: un bloque catch
normalmente espera y sabe cómo manejar el tipo de error particular, por lo que debería volver a lanzar errores que no conoce.
Incluso si no tenemos try...catch
, la mayoría de los entornos nos permiten configurar un controlador de errores "global" para detectar errores que "caen". En el navegador, eso es window.onerror
.
importancia: 5
Compare los dos fragmentos de código.
El primero usa finally
para ejecutar el código después de try...catch
:
intentar { trabajo trabajo } atrapar (errar) { manejar errores } finalmente { limpiar el espacio de trabajo }
El segundo fragmento realiza la limpieza justo después de try...catch
:
intentar { trabajo trabajo } atrapar (errar) { manejar errores } limpiar el espacio de trabajo
Definitivamente necesitamos la limpieza después del trabajo, no importa si hubo un error o no.
¿Hay alguna ventaja aquí en usar finally
o ambos fragmentos de código son iguales? Si existe tal ventaja, entonces dé un ejemplo cuando sea necesario.
La diferencia se vuelve obvia cuando miramos el código dentro de una función.
El comportamiento es diferente si hay un "salto" de try...catch
.
Por ejemplo, cuando hay un return
dentro try...catch
. La cláusula finally
funciona en caso de cualquier salida de try...catch
, incluso a través de la declaración return
: justo después de que try...catch
finaliza, pero antes de que el código de llamada obtenga el control.
función f() { intentar { alerta('inicio'); devolver "resultado"; } atrapar (errar) { ///... } finalmente { alerta('¡limpieza!'); } } F(); // limpieza!
…O cuando hay un throw
, como aquí:
función f() { intentar { alerta('inicio'); lanzar nuevo Error("un error"); } atrapar (errar) { //... if("no puedo manejar el error") { tirar errar; } } finalmente { alerta ('¡limpieza!') } } F(); // limpieza!
Es finally
lo que garantiza la limpieza aquí. Si simplemente pusiéramos el código al final de f
, no se ejecutaría en estas situaciones.