
Qué es un error personalizado
En JavaScript todos los errores descienden de la clase Error. Los tipos nativos (TypeError, ReferenceError, RangeError...) se usan para problemas del lenguaje, pero los errores de dominio de cada aplicación suelen necesitar más contexto: un campo que ha fallado una validación, el código HTTP que debería responderse, el identificador del recurso que no se encuentra, etc.
Para modelarlos, creamos subclases de Error. Cada subclase aporta un name distintivo y las propiedades que quiera exponer, y se puede detectar con instanceof en los bloques catch.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
try {
throw new ValidationError("El nombre es obligatorio");
} catch (error) {
console.log(error.name); // "ValidationError"
console.log(error.message); // "El nombre es obligatorio"
console.log(error instanceof ValidationError); // true
console.log(error instanceof Error); // true
}
Los tres puntos clave en cualquier subclase son: llamar a super(message) para que se inicialice correctamente, ajustar this.name (por defecto sería "Error") y añadir las propiedades que tu aplicación necesita.
Diseñar una jerarquía
Un único tipo de error suele no bastar. En aplicaciones con varias capas conviene definir una raíz propia y descender de ella para agrupar familias.
class AppError extends Error {
constructor(message, { statusCode = 500 } = {}) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
}
}
class ValidationError extends AppError {
constructor(message, { field } = {}) {
super(message, { statusCode: 400 });
this.field = field;
}
}
class NotFoundError extends AppError {
constructor(resource, id) {
super(`${resource} ${id} not found`, { statusCode: 404 });
this.resource = resource;
this.id = id;
}
}
class NetworkError extends AppError {
constructor(message, { statusCode = 502 } = {}) {
super(message, { statusCode });
}
}
La raíz AppError define las reglas comunes (cómo se calcula name, qué campos siempre estarán presentes) y cada subclase refina el significado.
Reaccionar según el tipo
Con una jerarquía bien definida, el bloque catch decide cómo responder según el tipo del error:
function handle(error) {
if (error instanceof ValidationError) {
return { status: 400, body: { field: error.field, message: error.message } };
}
if (error instanceof NotFoundError) {
return { status: 404, body: { resource: error.resource, id: error.id } };
}
if (error instanceof AppError) {
return { status: error.statusCode, body: { message: error.message } };
}
// Error desconocido: registrar y devolver 500 genérico
console.error("Unexpected error", error);
return { status: 500, body: { message: "Internal server error" } };
}
instanceof funciona con toda la cadena de herencia: un ValidationError también es AppError y Error. Por eso el orden de los if va de lo más específico a lo más general.
La propiedad cause (ES2022)
Antes de ES2022, para conservar el error original había que guardarlo a mano (wrapped.originalError = err). Ahora hay un campo estándar: el segundo argumento del constructor de Error acepta { cause }, que queda disponible como error.cause.
try {
JSON.parse("not-valid-json");
} catch (syntaxError) {
throw new AppError("No se pudo cargar la configuración", {
statusCode: 500
}) // 1º argumento del constructor
// Para encadenar con cause hay que pasarlo al super:
;
}
Para que cause llegue al Error base tenemos que pasarlo también al super. Reescribimos AppError para aprovecharlo:
class AppError extends Error {
constructor(message, { statusCode = 500, cause } = {}) {
super(message, { cause }); // <-- clave: pasar cause al super
this.name = this.constructor.name;
this.statusCode = statusCode;
}
}
Ahora podemos encadenar errores al saltar de una capa a otra:
async function loadConfig(path) {
try {
const raw = await fs.readFile(path, "utf8");
return JSON.parse(raw);
} catch (err) {
throw new AppError(`No se pudo cargar la configuración '${path}'`, {
statusCode: 500,
cause: err
});
}
}
Al capturar el error en el nivel superior, error.cause sigue apuntando al SyntaxError o ENOENT original:
try {
await loadConfig("./settings.json");
} catch (error) {
console.error(error.message); // "No se pudo cargar la configuración 'settings.json'"
console.error(error.cause?.message); // Mensaje original
console.error(error.cause); // SyntaxError, Error con code "ENOENT", etc.
}
Una cadena de errores puede ser arbitrariamente larga (un error con cause cuya cause tiene otra cause...). console.error en Node y en muchos navegadores imprime la cadena completa incluyendo los stacks de cada nivel, lo que facilita el diagnóstico.
Envolver errores de bajo nivel
El patrón habitual en aplicaciones con varias capas es envolver errores técnicos en errores de dominio al cruzar las fronteras.
class UserRepositoryError extends AppError {}
class UserRepository {
async findById(id) {
try {
return await db.query("SELECT * FROM users WHERE id = ?", [id]);
} catch (err) {
throw new UserRepositoryError(`Error consultando el usuario ${id}`, {
statusCode: 500,
cause: err
});
}
}
}
La capa HTTP, por encima, no necesita saber nada del motor de base de datos: atrapa un UserRepositoryError (o un AppError más genérico) y responde con 500. Si se quiere registrar la causa real, basta con error.cause.
De esta forma se respeta el principio de encapsulación: cada capa expone errores en el vocabulario de su dominio.
Discriminación por código de error
Cuando la jerarquía crece, instanceof puede resultar insuficiente. Una estrategia complementaria es añadir un campo code con valores estables:
class ValidationError extends AppError {
constructor(message, { code, field } = {}) {
super(message, { statusCode: 400 });
this.code = code;
this.field = field;
}
}
throw new ValidationError("Email duplicado", {
code: "EMAIL_ALREADY_REGISTERED",
field: "email"
});
Los code son cómodos para que el cliente de la API reaccione sin depender del texto del mensaje, y son fáciles de traducir.
Mantener un stack útil
new Error(...) captura automáticamente la pila en la propiedad stack. En la mayoría de motores modernos esto también funciona al extender. Si usas un motor en el que no, puedes capturarlo explícitamente con Error.captureStackTrace (V8 / Node):
class AppError extends Error {
constructor(message, options = {}) {
super(message, { cause: options.cause });
this.name = this.constructor.name;
this.statusCode = options.statusCode ?? 500;
if (typeof Error.captureStackTrace === "function") {
Error.captureStackTrace(this, this.constructor);
}
}
}
El truco Error.captureStackTrace(this, this.constructor) elimina el propio constructor de la pila, de modo que el primer frame que ves es el del throw en el código de llamada.
Buenas prácticas
- Define una raíz propia (
AppError,DomainError...) con los campos comunes y haz que todo el resto herede de ella. - Usa
this.name = this.constructor.nameen vez de hard-codear el nombre en cada subclase; así el nombre siempre coincide con la clase real. - Aprovecha
causepara encadenar errores al cruzar de una capa a otra. Pasar el error original comocausees estándar y se imprime automáticamente. - Añade
codede errores estables si vas a exponerlos por una API: es más fiable que elmessage. - Evita usar
throw "string". Lanza siempre objetosError(o subclases); así preservasname,stackycause. - No captures un error solo para registrarlo y relanzarlo tal cual: o lo manejas, o lo dejas subir. Si necesitas añadir contexto, envuélvelo con
cause.
Resumen
Crear subclases de Error permite expresar los problemas de una aplicación en el vocabulario de su dominio y reaccionar ante cada tipo de forma específica. Una jerarquía bien diseñada hace que los catch sean legibles: cada instanceof documenta cómo responde la aplicación ante ese caso. La propiedad cause de ES2022 completa el cuadro al preservar el error original detrás del error de alto nivel, facilitando el diagnóstico sin romper la abstracción. Juntas, estas dos ideas permiten un manejo de errores robusto, trazable y alineado con los principios de la programación orientada a objetos.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, JavaScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de JavaScript
Explora más contenido relacionado con JavaScript y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Crear subclases propias de
Errorcon nombre y propiedades significativas. - Diseñar una jerarquía (
AppError,ValidationError,NetworkError...). - Discriminar errores con
instanceofpara reaccionar de forma específica. - Usar la propiedad
causede ES2022 para encadenar errores. - Envolver errores de bajo nivel en errores de dominio sin perder el origen.
- Aplicar buenas prácticas de mensaje,
namey stack.