Por qué los errores son inevitables
Todo programa termina enfrentándose a una situación inesperada: un servidor devuelve un 500, un usuario escribe letras en un campo numérico, falta un archivo o una API de terceros cambia su estructura. El manejo de errores no consiste en prevenir cada fallo, sino en decidir qué sucede cuando ocurre uno.
Los errores bien gestionados mantienen la aplicación en funcionamiento, informan al usuario de manera útil y proporcionan a los desarrolladores los detalles necesarios. Por el contrario, los errores mal gestionados provocan pantallas en blanco, pérdida silenciosa de datos e informes de errores imposibles de reproducir.
El objeto Error
JavaScript tiene un tipo Error integrado, y cada error lanzado suele ser una instancia de este.
// error.js
const error = new Error("Something broke");
error.message; // "Something broke"
error.name; // "Error"
error.stack; // the call stack at the point of creation
error.cause; // an optional underlying error
Existen varias subclases integradas para categorías comunes: TypeError, ReferenceError, RangeError, SyntaxError, URIError y EvalError. Se diferencian principalmente en el nombre y en las situaciones que las provocan, lo que las hace útiles para la depuración y para crear bifurcaciones según el tipo de fallo.
Lanzar errores
Usa throw para señalar que se ha violado una regla. Lanza un objeto Error, nunca un string o un número; solo las instancias de Error incluyen un mensaje, un nombre y un stack trace.
// validate.js
function divide(a, b) {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
Lanzar el error lo antes posible, en el punto exacto donde se rompe una suposición, evita que un estado inválido se propague más profundamente en tu programa. Fallar ruidosamente durante el desarrollo es una ventaja, no un inconveniente.
try, catch y finally
try ejecuta código riesgoso. Si algo lanza una excepción, el control salta a catch. finally se ejecuta en cualquier caso, lo que lo convierte en el lugar ideal para realizar la limpieza.
// try.js
try {
const data = JSON.parse(input);
save(data);
} catch (error) {
console.error("Invalid data:", error.message);
} finally {
setLoading(false);
}
El parámetro catch es el valor lanzado. JavaScript moderno también admite el optional catch binding cuando no necesitas dicho valor: catch { ... }. Dentro de un bloque catch puedes inspeccionar el error, registrarlo en un log, recuperarte o volver a lanzarlo si no puedes manejarlo.
// rethrow.js
try {
await loadProfile();
} catch (error) {
log(error);
throw error; // let a higher layer decide
}
Clases de error personalizadas
Los errores genéricos pierden sentido a medida que tu aplicación crece. Las clases personalizadas te permiten adjuntar contexto y bifurcar la lógica según el tipo de fallo.
// http-error.js
class HttpError extends Error {
constructor(status, message) {
super(message ?? `HTTP ${status}`);
this.name = "HttpError";
this.status = status;
}
}
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
Ahora, quienes llamen a la función pueden reaccionar con precisión:
// handle.js
try {
await save(form);
} catch (error) {
if (error instanceof ValidationError) {
showFieldError(error.field, error.message);
} else if (error instanceof HttpError && error.status === 401) {
redirectToLogin();
} else {
showGenericError();
}
}
Esta es la diferencia entre un “algo salió mal” y un “el campo de correo electrónico ya está ocupado”.
Envolviendo errores con cause
A veces quieres añadir contexto sin perder el fallo original. La opción cause preserva la cadena.
// cause.js
try {
await db.query(sql);
} catch (error) {
throw new Error("Failed to load orders", { cause: error });
}
El error externo contiene el contexto legible para el usuario, mientras que error.cause mantiene el detalle original para los logs y la depuración.
Errores en código asíncrono
El código asíncrono tiene dos rutas de fallo adicionales. Con async/await, try/catch funciona exactamente igual que en el código síncrono.
// async.js
async function load() {
try {
const res = await fetch("/api/data");
if (!res.ok) throw new HttpError(res.status);
return await res.json();
} catch (error) {
console.error("Load failed:", error);
throw error;
}
}
Sin await, adjunta un manejador a la promesa:
// promise.js
fetch("/api/data")
.then((res) => res.json())
.catch((error) => console.error(error));
Una promesa rechazada sin un manejador se convierte en un unhandled rejection. En los navegadores, esto genera una advertencia; en Node.js, puede terminar el proceso. Maneja siempre los rechazos, aunque sea solo para registrarlos y volver a lanzarlos.
Redes de seguridad globales
Incluso con un código cuidadoso, algo se escapará. Los manejadores globales capturan lo que pasaste por alto para que la aplicación pueda reportarlo en lugar de morir silenciosamente.
// global.js
window.addEventListener("error", (event) => {
reportToServer(event.error);
});
window.addEventListener("unhandledrejection", (event) => {
reportToServer(event.reason);
});
En Node.js los equivalentes son process.on("uncaughtException") y process.on("unhandledRejection"). Trata estos manejadores como el último recurso para el registro de logs y el cierre controlado (graceful shutdown), no como un sustituto para manejar los errores donde ocurren.
Valida antes de lanzar
No todos los problemas son excepcionales. Las condiciones esperadas —una entrada vacía, un campo opcional faltante, una búsqueda sin resultados— se gestionan mejor mediante validación que con excepciones.
// guards.js
function formatName(user) {
if (!user?.name) return "Anonymous";
return user.name.trim();
}
Reserva las excepciones para situaciones genuinamente inesperadas o irrecuperables. Utilizarlas para el flujo de control ordinario hace que el código sea más lento y más difícil de seguir.
Comunicación con los usuarios
Un mensaje de error técnico es para los desarrolladores. Los usuarios necesitan saber qué sucedió y qué pueden hacer a continuación.
- Mantén los mensajes cortos y humanos: “No pudimos guardar tus cambios. Verifica tu conexión e inténtalo de nuevo”.
- Evita la jerga técnica, los stack traces y los códigos de estado raw en la interfaz.
- Proporciona un siguiente paso: reintentar, volver atrás, contactar al soporte o continuar sin conexión.
- Registra el detalle completo —mensaje, stack, contexto— para que el equipo de soporte pueda investigar.
Mejores prácticas
- Lanza objetos
Error, nunca strings. - Captura errores solo donde puedas recuperarte o añadir contexto útil.
- Usa clases de error personalizadas para fallos específicos del dominio.
- Envuelve los errores con
causeen lugar de descartar el original. - Maneja siempre las promise rejections.
- Añade handlers globales como red de seguridad final y registra los logs en un servicio real.
- Valida las condiciones esperadas en lugar de lanzar errores.
- Mantén los mensajes para el usuario separados de los diagnósticos para el desarrollador.
- Escribe tests para los flujos de fallo, no solo para el happy path.
Errores comunes
- Silenciar errores con un bloque
catchvacío. - Lanzar strings y perder el stack trace.
- Capturar un error y devolver un valor por defecto que oculta un bug real.
- Olvidar manejar promesas rechazadas.
- Mostrar mensajes de error sin procesar a los usuarios.
- Usar excepciones para el flujo de control ordinario.
- Asumir que
try/catchalrededor de un ejecutor de promesa captura errores asíncronos; no es así.
Próximos pasos
Los errores son el punto donde el código asíncrono y la Fetch API se encuentran con la realidad. Combina estos patrones con el DOM para renderizar estados amigables, y repasa los fundamentos de JavaScript cada vez que un TypeError te recuerde que undefined no es una función.