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.
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.
// 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:
// 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);messagees la descripción legible que tú le das.namees el tipo ("Error","TypeError"…).stackes 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:
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 deundefined…).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:
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");
}tryenvuelve lo que puede fallar.catch (error)recibe el error lanzado y decide qué hacer.finallyse 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.
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:
// 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:
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. -
causepara encadenar. Al re-lanzar, puedes guardar el error original como causa pasando un segundo argumento aError:{ cause: errorOriginal }. Así no pierdes el rastro de lo que empezó todo.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).
Paso 2: Que esté pulido
- validarHeroe lanza un Error con un mensaje útil en vez de devolver false.
- Capturas con try/catch y muestras el motivo de cada inválido.
Paso 3: Que sea excelente
- Defines un error propio (ErrorDeValidacion extends Error) con el campo que falló.
- Capturas por tipo con instanceof y re-lanzas lo que no reconoces (no tragas errores).
- Funciones puras y la presentación (consola) separada del cálculo.
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.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL MEJOR — "pulido"
//
// Por qué mejora a OK:
// - validarHeroe ya no devuelve un booleano mudo: LANZA un Error con un mensaje
// que dice qué regla se incumplió ("falta el nombre", "partidas negativas: -5").
// El motivo viaja con el error.
// - El bucle envuelve cada validación en try/catch: si pasa, es válido; si lanza,
// el catch recibe el error y muestra su mensaje. Un solo sitio decide qué hacer
// con el fallo.
//
// Su límite respecto a Excelente: todos los fallos son del mismo tipo (Error
// genérico), así que no podemos distinguir un error de validación de uno inesperado,
// ni saber qué CAMPO falló sin leer el texto del mensaje.
// ════════════════════════════════════════════════════════════════════════════
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 },
];
// Lanza un Error con mensaje útil en cuanto una regla se incumple. Si llega al
// final sin lanzar, el héroe es válido.
function validarHeroe(heroe) {
if (!heroe.nombre) throw new Error("falta el nombre");
if (heroe.partidas < 0)
throw new Error("partidas negativas: " + heroe.partidas);
if (heroe.victorias < 0 || heroe.victorias > heroe.partidas) {
throw new Error(
"victorias imposibles: " +
heroe.victorias +
" sobre " +
heroe.partidas +
" partidas",
);
}
// válido: devolvemos el propio héroe
return heroe;
}
console.log("— Validación —");
for (const heroe of candidatos) {
// si el nombre está vacío, usamos un texto de reemplazo para el log
const nombre = heroe.nombre || "(sin nombre)";
try {
// si lanza, saltamos al catch
validarHeroe(heroe);
console.log(nombre + ": válido");
} catch (error) {
// error.message trae el motivo concreto que lanzó validarHeroe.
console.log(nombre + ": inválido — " + error.message);
}
} Por qué es mejor que el anterior
- validarHeroe lanza un Error con un mensaje que dice qué regla se incumplió: el motivo viaja con el error.
- El bucle envuelve cada validación en try/catch: si pasa es válido, si lanza muestra el mensaje. Un solo sitio decide.
- Su límite: todos los fallos son del mismo tipo (Error genérico); no distingue un error de validación de uno inesperado.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL EXCELENTE — "óptimo"
//
// Por qué mejora a Mejor:
// - Un error PROPIO con tipo: class ErrorDeValidacion extends Error guarda además
// el `campo` que falló. Ahora el error no es solo un texto: es un objeto con
// estructura que el código puede inspeccionar.
// - El catch distingue POR TIPO con instanceof: si es un ErrorDeValidacion, lo
// trata como dato inválido y muestra el campo; si es CUALQUIER otro error (un bug
// nuestro, algo inesperado), lo RE-LANZA en vez de tragárselo. Un catch que se
// traga lo que no entiende esconde bugs reales.
// - Funciones puras (validar no imprime) y un bucle que solo presenta.
//
// Idea de fondo: un error bien diseñado lleva su tipo y sus datos. Quien lo captura
// decide con criterio, no leyendo strings a ojo.
//
// (instanceof sobre una subclase de Error es justo lo que el sandbox embebido
// transpila mal; en la consola de DevTools funciona de forma nativa.)
// ════════════════════════════════════════════════════════════════════════════
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 },
];
// Nuestro error de validación: hereda de Error y añade el campo que falló.
class ErrorDeValidacion extends Error {
constructor(mensaje, campo) {
// el mensaje lo gestiona el Error padre
super(mensaje);
// nuestro propio name
this.name = "ErrorDeValidacion";
// dato extra: qué campo incumplió la regla
this.campo = campo;
}
}
// Función pura: o devuelve el héroe válido, o lanza un ErrorDeValidacion con el campo.
function validarHeroe(heroe) {
if (!heroe.nombre) throw new ErrorDeValidacion("falta el nombre", "nombre");
if (heroe.partidas < 0) {
throw new ErrorDeValidacion(
"partidas negativas: " + heroe.partidas,
"partidas",
);
}
if (heroe.victorias < 0 || heroe.victorias > heroe.partidas) {
throw new ErrorDeValidacion(
"victorias fuera de rango: " + heroe.victorias,
"victorias",
);
}
// válido
return heroe;
}
// Valida un candidato y devuelve un resultado de presentación (sin imprimir nada).
function evaluar(heroe) {
const nombre = heroe.nombre || "(sin nombre)";
try {
validarHeroe(heroe);
return { nombre, ok: true };
} catch (error) {
// Solo tratamos como "inválido" lo que ES un error de validación.
if (error instanceof ErrorDeValidacion) {
return { nombre, ok: false, campo: error.campo, motivo: error.message };
}
// cualquier otro error es un bug: que suba, no lo tragamos
throw error;
}
}
console.log("— Validación —");
for (const heroe of candidatos) {
// datos -> resultado (cálculo separado de la presentación)
const r = evaluar(heroe);
if (r.ok) {
console.log(r.nombre + ": válido");
} else {
console.log(
r.nombre + ": inválido — campo " + r.campo + " (" + r.motivo + ")",
);
}
} Por qué es mejor que el anterior
- Un error propio con tipo: ErrorDeValidacion extends Error guarda además el campo que falló. El error lleva estructura, no solo texto.
- El catch distingue por tipo con instanceof: trata sus errores de validación y RE-LANZA cualquier otro en vez de tragárselo.
- Funciones puras (validar no imprime) y un bucle que solo presenta. Quien captura decide con criterio, no leyendo strings a ojo.