Por qué importan las excepciones explícitas
Los clientes HTTP dependen de códigos de estado estables y de formas de respuesta estables. Lanzar clases HttpError explícitas hace que los fallos sean observables, testeables y más fáciles de monitorear en producción. Cada clase de error exportada por el paquete tiene un código de estado semántico y un mensaje por defecto, así que el servidor puede normalizar respuestas sin lógica condicional en cada handler.
Cuando los controladores devuelven errores genéricos, los clientes pierden contexto semántico y el comportamiento de reintentos se vuelve frágil. El paquete evita esto al ofrecer una clase dedicada para cada escenario de fallo, todas extendiendo la misma base, lo que garantiza serialización consistente en toda la superficie API.
Clase base HttpError
Todos los errores de la jerarquía extienden HttpError, que a su vez extiende la clase nativa Error. Agrega una propiedad statusCode de solo lectura y sobrescribe el constructor para aceptar un mensaje y un estado HTTP numérico. Esta forma es el único contrato que la capa de normalización del servidor lee, así que cualquier clase que extienda HttpError participa automáticamente en el manejo centralizado de errores sin cableado adicional.
La clase base también define this.name con el nombre de la clase concreta, lo que hace que stack traces y dashboards de monitoreo distingan de inmediato los tipos de error. Las subclases solo deben llamar super(message, statusCode) con sus propios valores por defecto para mantener ese contrato.
Jerarquía de errores desde los exports del paquete
ValidationError y detalles estructurados
ValidationError es la única clase de error que transporta más que un mensaje. Incluye un arreglo details donde cada entrada mapea una propiedad a las restricciones que fallaron. Esta estructura se produce automáticamente por ValidationPipe cuando la validación de payloads de body pasa por reglas de class-validator.
La interfaz ValidationDetail expone dos campos: property (nombre del campo DTO) y constraints (un registro de nombres de regla fallidos a mensajes legibles para humanos). Como el pipeline usa whitelist: true y forbidNonWhitelisted: true, cualquier campo inesperado en el body también dispara error de validación, evitando que contaminación de payload llegue a lógica de negocio.
Los clientes reciben una forma predecible donde details siempre es un arreglo, incluso para un solo campo inválido. Esto permite iterar el arreglo y mapear cada entrada a errores inline de formularios o resúmenes de error sin bifurcar por forma de respuesta.
Patrones de excepción en controladores
import {
HttpBody,
HttpController,
HttpGet,
HttpParam,
HttpPatch,
HttpStatus,
} from '@spraxium/http';
import {
NotFoundError,
ForbiddenError,
ConflictError,
ServiceUnavailableError,
} from '@spraxium/http';
import { UpdateGuildConfigDto } from '../dto/update-guild-config.dto';
@HttpController('/guilds')
export class GuildHttpController {
@HttpGet('/:guildId')
async getGuild(@HttpParam('guildId') guildId: string) {
const guild = await this.findGuild(guildId);
if (!guild) {
throw new NotFoundError('Guild not found');
}
if (!guild.available) {
throw new ServiceUnavailableError('Guild is temporarily unavailable');
}
return { ok: true, guildId: guild.id, name: guild.name };
}
@HttpPatch('/:guildId/config')
@HttpStatus(200)
async updateConfig(
@HttpParam('guildId') guildId: string,
@HttpBody(UpdateGuildConfigDto) body: UpdateGuildConfigDto,
) {
const guild = await this.findGuild(guildId);
if (!guild) throw new NotFoundError('Guild not found');
if (guild.locked) {
throw new ForbiddenError('Guild configuration is locked by an admin');
}
if (guild.prefix === body.prefix) {
throw new ConflictError('Prefix is already set to that value');
}
return { ok: true, guildId, applied: body };
}
private async findGuild(\_guildId: string) {
return null;
}
}Comportamiento de normalización a nivel servidor
HttpServer registra dos handlers terminales al final del pipeline Hono: notFound y onError. Juntos garantizan que cada ruta posible de salida produzca una respuesta JSON con la misma forma.
El handler notFound se dispara para cualquier request que no coincida con una ruta registrada y devuelve { ok: false, error: 'Not Found' } con estado 404. El handler onError cubre excepciones lanzadas con esta precedencia:
- ValidationError devuelve estado 400 con
{ ok: false, error: 'Validation failed', details: [...] }; - cualquier otra subclase HttpError devuelve su propio status code con
{ ok: false, error: message }; - excepciones desconocidas se registran en logs y devuelven
{ ok: false, error: 'Internal server error' }con estado 500.
Como este mapeo está centralizado, los controladores deberían lanzar la clase de error más específica posible y nunca construir payloads JSON de error ad-hoc en código de handler. La capa de normalización se encarga del resto.
Contrato de forma de respuesta
Toda respuesta de error sigue el mismo envelope sin importar el código de estado. El campo ok siempre es false, el campo error lleva el string de mensaje y details aparece solo en respuestas de ValidationError. Esto permite que consumidores API verifiquen primero ok y luego bifurquen sobre error o details sin casos especiales por endpoint.
| Escenario | Estado | Cuerpo de respuesta |
|---|---|---|
| Ruta no coincidente | 404 | Objeto con ok: false y error Not Found |
| Fallo de validación DTO | 400 | Objeto con ok: false, error Validation failed y una lista details con property y constraints |
| Cualquier subclase de HttpError | Varía | Objeto con ok: false y el mensaje por defecto o personalizado de la clase |
| Excepción desconocida | 500 | Objeto con ok: false y error Internal server error |
Clases de error personalizadas
Puedes crear tus propias clases de error extendiendo HttpError o cualquier subclase existente. El único requisito es llamar super con mensaje y código de estado. Cualquier clase personalizada que extienda HttpError es capturada y serializada automáticamente por la capa de normalización del servidor sin cambios en middleware ni configuración.
import { ForbiddenError } from '@spraxium/http';
export class GuildLockedError extends ForbiddenError {
constructor(guildId: string) {
super(`Guild ${guildId} is locked and cannot be modified`);
this.name = 'GuildLockedError';
}
}Rechazo de guard como frontera de error
GuardExecutor ejecuta guards globales secuencialmente antes de cualquier handler de controlador. Cuando un guard retorna false, el ejecutor corta la ejecución y devuelve { ok: false, error: 'Unauthorized' } con estado 401. Esta respuesta se inyecta directamente, no a través de onError, lo que significa que los rechazos de guard son finales y no pueden capturarse ni modificarse con middleware posterior.
Como el comportamiento de guard está fuera del camino de normalización de HttpError, las implementaciones de guard deberían mantener semánticas de rechazo simples. La forma 401 coincide con el mismo envelope que todos los demás errores, por lo que los clientes no necesitan tratamiento especial para fallos de guard.
Checklist de diseño de excepciones
Lanza errores semánticos cerca de reglas de negocio
Lanza clases HttpError específicas donde se toman decisiones de dominio, no solo en el borde del controlador. Lanzar ConflictError dentro de lógica de servicio es más claro que retornar un flag y convertirlo en error más tarde en el handler.
Mantén mensajes accionables y estables
Los mensajes de error deberían ayudar a clientes y operadores a entender siguientes pasos sin filtrar detalles internos sensibles. Un wording estable también mejora deduplicación de alertas y calidad de dashboards con el tiempo.
Usa validación DTO para corrección de forma
Deja que ValidationPipe y class-validator impongan garantías estructurales antes de ejecutar lógica de dominio. El pipeline usa whitelist: true y forbidNonWhitelisted: true por defecto, lo que significa que propiedades inesperadas se rechazan automáticamente.
Prefiere subclases personalizadas sobre HttpError crudo
Crea subclases con nombres descriptivos como GuildLockedError o BridgeUnavailableError en lugar de lanzar ForbiddenError crudo con un string. Las clases con nombre son más fáciles de buscar en logs, testear en aserciones y filtrar en dashboards de monitoreo.
Apóyate en mapeo centralizado de respuestas
Permite que HttpServer normalice errores a JSON de forma consistente en lugar de construir payloads ad-hoc en cada handler. Un mapeo unificado es más fácil de testear y razonar entre equipos, y reduce superficie para desalineaciones accidentales en forma de respuesta.
Nunca expongas detalles internos de error a clientes
Excepciones desconocidas son capturadas por la capa de normalización y devueltas como respuesta genérica 500. El mensaje original del error solo se registra del lado servidor. No sobrescribas este comportamiento para reenviar stack traces crudos o mensajes internos, porque eso filtra detalles de implementación a consumidores externos.