learning-front

Nivel 3 · JavaScript moderno y asíncrono

Errores a fondo

Lanzar y capturar errores con criterio: throw, el objeto Error y sus subclases, errores propios con extends y cómo se propagan por la pila hasta quien los atrapa.

Hasta ahora, cuando algo iba mal, tu código tenía dos salidas pobres: devolver null o false y confiar en que quien llama se acuerde de comprobarlo, o dejar que el programa reviente con un error del navegador que no controlas. Ninguna sirve para un proyecto serio. JavaScript tiene un mecanismo dedicado a esto: lanzar un error cuando algo no cuadra y capturarlo donde tenga sentido tratarlo. Es la diferencia entre “se rompió, no sé por qué” y “falló la validación del campo partidas, y aquí está el motivo”.

Seguimos con el Overwatch Team Builder. Vamos a validar datos de héroes —nombres, partidas, victorias— que pueden venir mal, y a reportar cada fallo con precisión en vez de tragárnoslo.

throw: lanzar un error#

throw lanza un error: interrumpe la función en ese punto (lo que viene después no se ejecuta) y el control salta a quien lo capture. Técnicamente puedes lanzar cualquier valor (throw 'texto', throw 42), pero lanza siempre un objeto Error: trae mensaje, tipo y traza, y es lo que espera todo el código que captura.

javascript
function comprobarPartidas(heroe) {
  // Si el dato no cumple la regla, lanzamos y cortamos aquí mismo.
  if (heroe.partidas < 0) {
    throw new Error("partidas no puede ser negativo: " + heroe.partidas);
  }
  // Esta línea solo se ejecuta si NO se lanzó nada arriba.
  return heroe.partidas;
}

Compáralo con devolver false: un throw no se puede ignorar por descuido. Si nadie lo captura, el programa se detiene gritando el problema, en vez de seguir con un dato malo como si nada.

Cuándo lanzar y cuándo devolver un valor#

Antes de lanzar, párate un momento: ¿es esto realmente excepcional?

  • Lanza cuando el sistema encuentra algo que no debería poder ocurrir en un uso normal: un dato de base de datos corrupto, una operación imposible, una precondición rota.
  • Devuelve un valor (un booleano, null, un resultado vacío) cuando el caso es parte del flujo esperado: buscar un héroe que simplemente puede no existir, o comprobar si un formulario es válido.
javascript
// El array donde buscamos héroes.
const roster = [
  { nombre: "Tracer", partidas: 120, victorias: 78 },
  { nombre: "Mercy", partidas: 200, victorias: 130 },
];

// Flujo normal: "puede no estar" no es excepcional -> devuelve null, no lances.
function buscarHeroe(nombre) {
  // busca en el array
  const heroe = roster.find((h) => h.nombre === nombre);
  // si no lo encontró, find devuelve undefined; devolvemos null explícito
  return heroe !== undefined ? heroe : null;
}

// Situación realmente excepcional: las victorias no pueden superar las partidas -> lanza.
function registrarPartida(heroe) {
  if (heroe.victorias > heroe.partidas) {
    // esto no debería ocurrir
    throw new Error("dato imposible: victorias > partidas");
  }
  // incrementa el contador
  heroe.partidas++;
}

La regla práctica: si el que llama a tu función necesita saber el motivo para decidir qué hacer, lanza. Si solo necesita saber “sí o no”, devuelve un booleano o null.

El objeto Error: message, name y stack#

Un Error es un objeto normal con tres propiedades clave:

javascript
// crearlo NO lo lanza; solo lo construye
const fallo = new Error("algo se rompió");

// "algo se rompió"  (el texto que le pasaste)
console.log(fallo.message);
// "Error"              (el tipo)
console.log(fallo.name);
// la traza: la cadena de llamadas hasta donde se creó
console.log(fallo.stack);
  • message es la descripción legible que tú le das.
  • name es el tipo ("Error", "TypeError"…).
  • stack es la traza: la lista de funciones por las que se pasó hasta el punto del fallo. Es lo que lees en la consola para localizar dónde se originó.

Errores nativos: TypeError, RangeError#

El propio lenguaje lanza errores cuando algo va mal de forma típica, y usa subclases de Error con un name más específico:

javascript
const numero = 5;
// TypeError: numero.toUpperCase is not a function
numero.toUpperCase();

const array = [1, 2, 3];
// RangeError: Invalid array length
array.length = -1;
  • TypeError: usaste un valor de un tipo que no toca (llamar a algo que no es función, leer una propiedad de undefined…).
  • RangeError: un valor está fuera del rango permitido.

No tienes que memorizarlos: lo importante es que todos heredan de Error, así que comparten message, name y stack, y los capturas igual.

try / catch / finally: capturar el error#

Para que un error no tumbe el programa, envuelves el código que puede fallar en un try y tratas el fallo en el catch:

javascript
try {
  // Código que PUEDE lanzar.
  // esto lanza
  comprobarPartidas({ partidas: -3 });
  // saltado: el throw cortó el try
  console.log("no llego aquí");
} catch (error) {
  // Solo se ejecuta si el try lanzó. `error` es lo que se lanzó.
  console.log("Falló: " + error.message);
} finally {
  // Se ejecuta SIEMPRE: haya ido bien o haya saltado al catch.
  console.log("Comprobación terminada");
}
  • try envuelve lo que puede fallar.
  • catch (error) recibe el error lanzado y decide qué hacer.
  • finally se ejecuta pase lo que pase: es el sitio para lo que hay que hacer en cualquier caso (cerrar algo, apagar un “cargando”).

Este mismo try/catch funciona también con código asíncrono. En el capítulo de Promesas y async/await la estructura es idéntica, solo que dentro de funciones asíncronas.

La propagación: el error sube por la pila#

Esto es lo que hace a los errores tan cómodos: un error que no se captura sube por la pila de llamadas hasta que alguien lo atrapa. No necesitas un try/catch en cada función; basta uno arriba que cubra todo lo que se llama por debajo.

javascript
function leerVictorias(heroe) {
  if (heroe.victorias > heroe.partidas) {
    // (3) se lanza aquí, abajo del todo
    throw new Error("victorias imposibles");
  }
  return heroe.victorias;
}

function procesar(heroe) {
  // (2) NO captura: el error la atraviesa y sigue subiendo
  return leerVictorias(heroe);
}

try {
  // (1) la llamamos desde aquí
  procesar({ victorias: 99, partidas: 40 });
} catch (error) {
  // (4) el error subió de leerVictorias a procesar y hasta este catch.
  // victorias imposibles
  console.log("Capturado arriba: " + error.message);
}

El error nace en leerVictorias, atraviesa procesar (que no lo captura) y llega al try/catch de fuera. Por eso pones el catch donde puedes hacer algo útil con el fallo, no necesariamente donde se origina.

Errores propios: class ... extends Error#

Un Error genérico te dice que algo falló, pero no de qué tipo es ni con qué datos. Para eso creas tu propio error heredando de Error (con las clases del nivel 2) y le añades lo que necesites:

javascript
// Hereda de Error y añade un campo propio: el campo que falló la validación.
class ErrorDeValidacion extends Error {
  constructor(mensaje, campo) {
    // pasa el mensaje al Error padre (rellena this.message)
    super(mensaje);
    // nuestro propio name, en vez de "Error"
    this.name = "ErrorDeValidacion";
    // dato extra: solo nuestro
    this.campo = campo;
  }
}

function validarHeroe(heroe) {
  if (!heroe.nombre) {
    throw new ErrorDeValidacion("falta el nombre", "nombre");
  }
  return true;
}

Ahora el error no es solo un texto: es un objeto con tipo (ErrorDeValidacion) y estructura (error.campo). El código que lo capture puede inspeccionarlo, no solo leer el mensaje.

Capturar por tipo con instanceof (y re-lanzar)#

instanceof comprueba de qué clase es un objeto. En un catch, te deja distinguir tus errores de los inesperados y tratarlos distinto:

javascript
try {
  // lanza un ErrorDeValidacion
  validarHeroe({ nombre: "" });
} catch (error) {
  // ¿Es uno de NUESTROS errores de validación?
  if (error instanceof ErrorDeValidacion) {
    // lo tratamos como dato malo
    console.log("Campo inválido: " + error.campo);
  } else {
    // No lo reconocemos: es un bug. Lo RE-LANZAMOS para que suba, no lo escondemos.
    throw error;
  }
}

Ese throw error del else es clave: no te tragues lo que no entiendes. Un catch que captura todo y no hace nada (o solo lo de validación) esconde bugs reales bajo la alfombra. Captura lo que sabes tratar; deja subir el resto.

Buenas prácticas#

  • No te tragues los errores. Un catch {} vacío convierte un fallo en un silencio: lo peor que puede pasarte al depurar. Si capturas, haz algo (trátalo, regístralo) o re-lánzalo.

  • Mensajes útiles. throw new Error("partidas: -5") ayuda; throw new Error("error") no.

  • cause para encadenar. Al re-lanzar, puedes guardar el error original como causa pasando un segundo argumento a Error: { cause: errorOriginal }. Así no pierdes el rastro de lo que empezó todo.

    javascript
    try {
      // puede lanzar un ErrorDeValidacion
      validarHeroe(candidato);
    } catch (errorOriginal) {
      // Envolvemos el error en uno nuevo con más contexto, sin perder el original.
      throw new Error("no se pudo procesar el héroe: " + candidato.nombre, {
        // el error de partida queda guardado en .cause
        cause: errorOriginal,
      });
      // Quien capture este nuevo error puede leer error.cause para ver la raíz.
    }

Pruébalo tú#

Edita el código de abajo y pulsa «Ejecutar»: el editor corre class ... extends Error e instanceof de forma nativa. Fíjate en cómo throw corta la ejecución, cómo el catch recoge el error y cómo instanceof distingue tu ErrorDeValidacion.

Comprueba lo que sabes#

Pregunta 1 de 5

¿Qué hace throw cuando se ejecuta?

Tu turno#

Leerlo no es lo mismo que escribirlo. Resuélvelo aquí mismo: edita el código de partida, valida una lista de candidatos con datos sucios y reporta cada fallo con precisión. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en cómo el nivel Excelente diseña un error con tipo y lo captura con instanceof sin tragarse lo inesperado.

Ejercicio · en esta página

Validación robusta de héroes

Recibes una lista de candidatos al roster, algunos con datos malos. Valida cada uno (nombre, partidas no negativas, victorias entre 0 y partidas) y reporta los fallos con claridad. En los niveles altos, lanza errores con criterio y captúralos por tipo, sin tragarte lo inesperado.

Paso 1: Que funcione

  • Validas las tres reglas (nombre, partidas, victorias).
  • Cada candidato se ve como válido o inválido (vale devolver un booleano).
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// NIVEL OK — "funciona"
//
// validarHeroe devuelve true o false. El bucle muestra "válido" o "inválido"
// por candidato y, efectivamente, no deja pasar los datos malos.
//
// Su límite: un booleano es MUDO. Cuando algo es inválido, quien llama no sabe
// QUÉ falló (¿el nombre? ¿las partidas?) ni puede reaccionar distinto según el
// motivo. Para eso no basta un false: hace falta un error con información.
// ════════════════════════════════════════════════════════════════════════════
const candidatos = [
  { nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
  { nombre: "", rol: "Apoyo", partidas: 200, victorias: 130 },
  { nombre: "Genji", rol: "Daño", partidas: -5, victorias: 0 },
  { nombre: "Echo", rol: "Daño", partidas: 40, victorias: 99 },
  { nombre: "Ana", rol: "Apoyo", partidas: 140, victorias: 88 },
];

// Devuelve true si cumple TODAS las reglas; false en cuanto incumple una.
function validarHeroe(heroe) {
  // sin nombre
  if (!heroe.nombre) return false;
  // partidas negativas
  if (heroe.partidas < 0) return false;
  // victorias imposibles
  if (heroe.victorias < 0 || heroe.victorias > heroe.partidas) return false;
  // pasó todas las reglas
  return true;
}

// Por cada candidato, un booleano: no sabemos por qué falla, solo que falla.
console.log("— Validación —");
for (const heroe of candidatos) {
  // true / false, sin más
  const ok = validarHeroe(heroe);
  const nombre = heroe.nombre || "(sin nombre)";
  console.log(nombre + ": " + (ok ? "válido" : "inválido"));
}

Por qué este nivel

  • validarHeroe devuelve true o false, y el bucle no deja pasar los datos malos. Funciona.
  • Su límite: un booleano es mudo. Cuando algo es inválido, quien llama no sabe QUÉ falló.
  • No puede reaccionar distinto según el motivo: para eso un false no basta.