Manejo de errores en JavaScript

Aprende como evitar el 💥¡CRASH!💥 en tus programas, controla los posibles errores en tu código.

Luis Angel Mendoza Lucio

Aprende como evitar el 💥¡CRASH!💥 en tus programas, controla los posibles errores en tu código.

try...catch... define un bloque de expresiones para intentar ejecutar y otro bloque para agregar la forma en la que vamos a responder si se presenta un error.

Alguna vez hemos llegado a ver algo así:

const inicio = 1810;

console.log('La lucha de independencia duró :' + (fin  - inicio) + ' años.');

iniciarBatalla();

Con solo un vistazo sabemos que eso va a crashear.

Error fin is not defined

Como nosotros no especificamos que hacer en caso de que se presente un error en esas líneas de código, el propio lenguaje se encarga de procesarlo y el navegador de mostrarlo, pero con eso se ha detenido la ejecución del programa. Lo que significa que las siguientes líneas de código ya no se ejecutarán. (adios a la ejecución de la función iniciarBatalla)

Vamos a ver la estructura del try...catch...

try {
	// Código a ejecutar
} catch (e) {
    // En caso de que alguna línea falle, el flujo pasará a este lugar
}

Así de sencillo es proteger nuestros programas y poder tener siempre el control del flujo.

try {
	const inicio = 1810;
	console.log('La lucha de independencia duró :' + (fin  - inicio) + ' años.');
} catch (e) {
	console.log('Ups. Ha ocurrido un error al imprimir el valor');
}

Con el código anterior ahora no sale el molesto mensaje de error del navegador, ahora tomamos el error y mostramos en consola un mensaje sobre él. Tal vez te preguntes que tiene la variable e dentro del bloque catch
pues vamos a ver...

La variable e nos da información referente al error que ha ocurrido. Normalmente, es un objeto que tiene la propiedad name y la propiedad message, con el nombre del tipo de error y el mensaje de error, respectivamente. 👁️ ¡Ojo! Esta variable solo podrá ser utilizada en el bloque catch

Entonces veamos el código siguiente...

try {
	const inicio = 1810;
	console.log('La lucha de independencia duró :' + (fin  - inicio) + ' años.');
} catch (e) {
	console.log(e.name); // ReferenceError
    console.log(e.message); // fin is not defined
}

Podemos ver que efectivamente el objeto e tiene esas 2 propiedades que dependiendo la situación nos servirán para reaccionar de una o de otra forma al error ocurrido.

try {
	const inicio = 1810;
	console.log('La lucha de independencia duró :' + (fin  - inicio) + ' años.');
} catch ({name, message}) {
	console.log(name); // ReferenceError
    console.log(message); // fin is not defined
}

En el código anterior vemos el mismo ejemplo pero ahora desestructurando el objeto e para obtener las propiedades y evitar utilizar e.name o e.message. 👁️ ¡Ojo! La variable name y message estarán reservadas dentro del bloque catch entonces no podremos tener otras variables con esos mismos nombres dentro del bloque.

Vamos a ver un poco más sobre el bloque catch...

try {
	const inicio = 1910;
	if (inicio !== 1810) {
    	throw 'Hay un error en la fecha inicial';
    }
} catch (e) {
	console.log(e); // Hay un error en la fecha inicial
    console.log(e.name); // undefined
    console.log(e.message); // undefined
}

¿Qué paso aquí? Como vemos en el código anterior, nosotros mismo lanzamos el error con la instrucción throw y solo mandamos una cadena de texto, esto ya no es un objeto Error, por lo tanto, dentro del bloque catch la variable e es un simple string y no tiene ni la propiedad name ni message.

try {
	throw {
    	name: 'Nombre de error propio', 
    	message: 'Ha ocurrido un error', 
    	time: new Date().getTime()
    };
} catch (e) {
	console.log(e);
}

En el código anterior vemos que así como podíamos provocar el error y pasar solo una cadena de texto, también podemos pasar un objeto personalizado, en este caso pasamos el tiempo en que ocurrió el error. (es solo un ejemplo para mostrar que se puede enviar un objeto con cualquier propiedad)

Vamos a ver un bloque más que acompaña a try...catch... y es finally...
El bloque finally tendrá las instrucciones que queramos que sean ejecutadas una vez concluidos el bloque try y/o el bloque catch.

try {
  throw new Error('Ha ocurrido un error');
} catch (e) {
  console.log(e.message);
} finally {
  console.log('Me ejecutaré ocurra o un error');
}
// SALIDA
// Ha ocurrido un error
// Me ejecutaré ocurra o un error

El bloque finally nos puede ser útil, por ejemplo, si tenemos una petición al servidor, podríamos querer mostrar una ventana de carga para indicar que se está cargando información, y claro está que una vez obtenidos los datos del servidor querremos dejar de mostrar la ventana de carga.

function mostrarInformacion() {
  try {
    mostrarVentanaCarga('Cargando batallas ...');
    obtenerBatallas();
    ocultarVentanaCarga();
  } catch (e) {
    console.log(e);
  }
}
mostrarInformacion();

Pero tal vez también queramos quitar la ventana de carga si es que hubo un error, para poder en ese caso mostrar el mensaje indicando el error ocurrido. Podríamos hacer algo así...

function mostrarInformacion() {
  try {
    mostrarVentanaCarga('Cargando batallas ...');
    obtenerBatallas();
    ocultarVentanaCarga();
  } catch (e) {
    console.log(e);
    ocultarVentanaCarga();
  }
}
mostrarInformacion();

Pero podríamos mejor utilizar el bloque finally y así, independientemente de si la ejecución falla o no, siempre cerraremos la ventana de carga. Es un ejemplo sencillo, pero bueno para entender como funciona el bloque finally.

function mostrarInformacion() {
  try {
    mostrarVentanaCarga('Cargando batallas ...');
    obtenerBatallas();
  } catch (e) {
    console.log(e);
  } finally {
    ocultarVentanaCarga();
  }
}
mostrarInformacion();

Ahora veremos que pasa con bloques try...catch...finally anidados.

try {
  // try exterior
  throw new Error('Ha ocurrido un error en el try externo');
  try {
    // try interior
    throw new Error('Ha ocurrido un error en el try interno');
  } catch(e) {
    // catch interior
    console.log('interno: ' + e.message);
  }
} catch (e) {
  // catch exterior
  console.log('externo: ' + e.message);
}
// SALIDA:
// externo: Ha ocurrido un error en el try externo

En el código de arriba podemos ver directamente el resultado. Lo que pasa es que una vez dentro del try interno se lanza un error con throw, entonces 'busca' si existe un bloque catch que pueda tomar ese error y actuar sobre él, y en este caso claro que lo encuentra y es su bloque catch el que tiene la palabra 'externo'.

try {
  // try exterior
  try {
    // try interior
    throw new Error('Ha ocurrido un error en el try interno');
  } catch(e) {
    // catch interior
    console.log('interno: ' + e.message);
  }
} catch (e) {
  // catch exterior
  console.log('externo: ' + e.message);
}
// SALIDA:
// interno: Ha ocurrido un error en el try interno

Ahora en el ejemplo de arriba el try exterior ya no es quien lanza el error sino el try interior y pasa lo mismo, 'busca' si existe el bloque catch para que tome el error, lo encuentra y ya vemos lo que pasa. Pero que tal con el siguiente código...

try {
  // try exterior
  try {
    // try interior
    throw new Error('Ha ocurrido un error en el try interno');
  } finally {
    console.log('Finally interno');
  }
} catch (e) {
  // catch exterior
  console.log('externo:', e.message);
}
// SALIDA:
// Finally interno
// externo:' 'Ha ocurrido un error en el try interno

¿Qué ha pasado? Fácil, el try interno 'busca' a su bloque catch, al no encontrarlo el error 'viaja' al catch exterior que es quien toma el error y lo muestra. Claro está que si no se encuentra un bloque catch, no habrá quien tome el error para actuar en consecuencia.

👁️ ¡Ojo! Puedes haber notado que se agregó el bloque finally al try interno, esto es porque un bloque try forzosamente debe estar acompañado de un bloque catch o un bloque finally o marcará un error de sintaxis.

Hay algo más que contar sobre el bloque finally y es lo que pasa cuando se usa la instrucción return dentro del bloque.

(function() {
  
})();

Para el ejemplo vamos a crear una función auto ejecutada, no te preocupes si no entiendes esto, quédate por ahora con la idea de que es una función que se llama de forma automática, es importante trabajar con una función para este ejemplo porque usaremos la palabra clave return.

(function() {
  try {
    throw new Error('Lanzamos un error');
  } catch(e) {
    console.log('Obtenemos el error: ' + e.message);
  } finally {
    console.log('Se finaliza');
    return 'soy un valor retornado desde finally';
  }
})();
// SALIDA:
// Obtenemos el error: Lanzamos un error
// Se finaliza
// soy un valor retornado desde finally

El valor que retorne el bloque finally se convierte en el valor de retorno de toda la estructura try...catch...finally..., no importa que haya un return dentro del bloque try o dentro del bloque catch, si existe un return en el bloque finally se tomará solo ese, veámoslo en un ejemplo...

(function() {
  try {
    throw new Error('Lanzamos un error');
  } catch(e) {
    console.log('Obtenemos el error: ' + e.message);
    return 'Soy un valor retornado desde catch';
  } finally {
    console.log('Se finaliza');
    return 'soy un valor retornado desde finally';
  }
})();
// SALIDA:
// Obtenemos el error: Lanzamos un error
// Se finaliza
// soy un valor retornado desde finally

Obtenemos exactamente el mismo resultado, porque se ignoró el return dentro del bloque catch y se tomo el que está dentro del bloque finally.

(function () {
  try {
    // try exterior
    try {
      // try interior
      throw new Error('Lanzamos un error');
    } catch (e) {
      // catch interior
      console.log('Catch interior: ' + e.message);
      return 'Soy un valor retornado desde catch interior';
    } finally {
      // finally interior
      return 'soy un valor retornado desde finally interior';
    }
  } catch (e) {
    // catch exterior
    console.log('Catch exterior: ' + e.message);
    return 'Soy un valor retornado desde catch exterior';
  } finally {
    // finally exterior
    console.log('Finally exterior');
  }
})();
// SALIDA:
// Catch interior: Lanzamos un error
// Finally exterior
// soy un valor retornado desde finally interior

En el ejemplo podemos ver que en el bloque finally interno se ejecuta un return, como se terminó de ejecutar la estructura try...catch...finally... interior, el valor retornado se queda en espera de que la estructura try...catch...finally... exterior se termine de ejecutar y cuando eso pasa es que llega el turno delreturn del finally interior y se muestra en consola. Es por eso que se muestra primero el mensaje 'Finally exterior' que el valor retornado por el finally interior.

(function () {
  try {
    // try exterior
    try {
      // try interior
      throw new Error('Lanzamos un error');
    } catch (e) {
      // catch interior
      console.log('Catch interior: ' + e.message);
      return 'Soy un valor retornado desde catch interior';
    } finally {
      // finally interior
      console.log('Finally interior');
      return 'soy un valor retornado desde finally interior';
    }
  } catch (e) {
    // catch exterior
    console.log('Catch exterior: ' + e.message);
    return 'Soy un valor retornado desde catch exterior';
  } finally {
    // finally exterior
    console.log('Finally exterior');
    return 'soy un valor retornado desde finally exterior';
  }
})();
// SALIDA:
// Catch interior: Lanzamos un error
// Finally interior
// Finally exterior
// soy un valor retornado desde finally exterior

Pero en este último ejemplo sucede algo similiar, con la unica diferencia que al haber un return en el bloque finally exterior es el que se retornará, ignorando el return del bloque finally interior.

Es importante proteger el flujo de nuestros programas para poder reaccionar a los siempre inoportunos errores, una cosa es que ocurra un error y otra es que ese error nos rompa el programa. Debemos capturar los errores, procesarlos y no dejar que el usuario sea interrumpido o que su experiencia sea desastrosa. Espero que este post te sea un poco de utilidad.

Programación

Luis Angel Mendoza Lucio

Líder de proyectos de frontend, aunque es una persona reservada, le encanta compartir su conocimiento y experiencia con otros, también es ávido lector, y disfruta investigar sobre la historia.

Comentarios