Por que exceções explícitas importam
Clientes HTTP dependem de status codes estáveis e formatos de resposta estáveis. Lançar classes explícitas de HttpError torna falhas observáveis, testáveis e mais fáceis de monitorar em produção. Toda classe de erro exportada pelo pacote carrega um status code semântico e uma mensagem padrão, então o servidor consegue normalizar respostas sem lógica condicional em cada handler.
Quando controllers retornam erros genéricos, clientes perdem contexto semântico e o comportamento de retry fica frágil. O pacote evita isso ao dar a cada cenário de falha uma classe dedicada que estende a mesma base, garantindo serialização consistente em toda a superfície da API.
HttpError base class
Todo erro da hierarquia estende HttpError, que por sua vez estende a classe nativa Error. Ela adiciona uma propriedade statusCode somente leitura e sobrescreve o construtor para aceitar uma mensagem e um status HTTP numérico. Esse formato é o único contrato que a camada de normalização do servidor lê, então qualquer classe que estenda HttpError participa automaticamente do tratamento centralizado de erros sem wiring adicional.
A classe base também define this.name com o nome da classe concreta, o que torna stack traces e dashboards de monitoramento imediatamente distinguíveis entre tipos de erro. Subclasses só precisam chamar super(message, statusCode) com seus próprios padrões para manter esse contrato.
Hierarquia de erros exportada pelo pacote
ValidationError e detalhes estruturados
ValidationError é a única classe de erro que carrega mais do que uma mensagem. Ela inclui um array details em que cada entrada mapeia o nome de uma propriedade para as constraints que falharam. Essa estrutura é produzida automaticamente por ValidationPipe quando a validação do payload de body roda pelas regras de class-validator.
A interface ValidationDetail expõe dois campos: property, o nome do campo do DTO, e constraints, um registro que mapeia nomes de regras que falharam para mensagens legíveis por humanos. Como o pipeline usa whitelist: true e forbidNonWhitelisted: true, qualquer campo inesperado no body também dispara um erro de validação, impedindo que poluição de payload chegue à regra de negócio.
Clientes recebem um formato previsível em que details é sempre um array, mesmo quando há só um campo inválido. Isso permite que consumidores da API percorram o array e mapeiem cada entrada para erros inline de formulário ou resumos de erro sem precisar ramificar pela forma da resposta.
Padrões de exceção em controllers
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;
}
}Comportamento de normalização no nível do servidor
HttpServer registra dois handlers terminais no fim do pipeline do Hono: notFound e onError. Juntos, eles garantem que toda saída possível produza uma resposta JSON com o mesmo formato.
O handler notFound dispara para qualquer requisição que não corresponda a uma rota registrada e retorna { ok: false, error: 'Not Found' } com status 404. O handler onError cobre exceções lançadas usando esta precedência:
- ValidationError retorna status 400 com
{ ok: false, error: 'Validation failed', details: [...] }; - qualquer outra subclasse de HttpError retorna seu próprio status code com
{ ok: false, error: message }; - exceções desconhecidas são registradas em log e retornadas como
{ ok: false, error: 'Internal server error' }com status 500.
Como esse mapeamento é centralizado, controllers devem lançar a classe de erro mais específica disponível e nunca montar payloads JSON de erro ad hoc dentro do handler. A camada de normalização cuida do restante.
Contrato do formato de resposta
Toda resposta de erro segue o mesmo envelope, independentemente do status code. O campo ok é sempre false, o campo error carrega a mensagem em string e details aparece apenas em respostas de ValidationError. Isso permite que consumidores da API verifiquem primeiro ok e depois desviem para error ou details sem tratamento especial por endpoint.
| Cenário | Status | Corpo da resposta |
|---|---|---|
| Rota sem correspondência | 404 | Objeto com ok: false e erro Not Found |
| Falha de validação de DTO | 400 | Objeto com ok: false, erro Validation failed e uma lista details com property e constraints |
| Qualquer subclasse de HttpError | Varia | Objeto com ok: false e a mensagem padrão ou customizada da classe |
| Exceção desconhecida | 500 | Objeto com ok: false e erro Internal server error |
Classes de erro customizadas
Você pode criar suas próprias classes de erro estendendo HttpError ou qualquer subclasse existente. A única exigência é chamar super com uma mensagem e um status code. Qualquer classe customizada que estenda HttpError é capturada e serializada automaticamente pela camada de normalização do servidor, sem mudanças em middleware ou configuração.
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';
}
}Rejeição de guard como fronteira de erro
GuardExecutor executa guards globais em sequência antes de qualquer handler de controller. Quando um guard retorna false, o executor interrompe a execução e retorna { ok: false, error: 'Unauthorized' } com status 401. Essa resposta é injetada diretamente, e não pelo handler onError, o que significa que rejeições de guard são finais e não podem ser capturadas nem modificadas por middleware posterior.
Como o comportamento de guard fica fora do caminho de normalização de HttpError, implementações de guard devem manter a semântica de rejeição simples. O formato da resposta 401 segue o mesmo envelope dos demais erros, então clientes não precisam de tratamento especial para falhas de guard.
Checklist de design para exceções
Lance erros semânticos perto das regras de negócio
Lance classes específicas de HttpError onde as decisões de domínio são tomadas, e não apenas na borda do controller. Lançar um ConflictError dentro da lógica de service é mais claro do que retornar uma flag e convertê-la depois em erro no handler.
Mantenha mensagens acionáveis e estáveis
Mensagens de erro devem ajudar clientes e operadores a entender os próximos passos sem vazar detalhes internos sensíveis. Uma redação estável também melhora deduplicação de alertas e a qualidade dos dashboards ao longo do tempo.
Use validação de DTO para garantir o formato
Deixe ValidationPipe e class-validator impor garantias estruturais antes
de a lógica de domínio rodar. O pipeline usa whitelist: true e
forbidNonWhitelisted: true por padrão, o que significa que propriedades
inesperadas são rejeitadas automaticamente.
Prefira subclasses customizadas a HttpError bruto
Crie subclasses com nomes descritivos como GuildLockedError ou BridgeUnavailableError em vez de lançar um ForbiddenError puro com uma string. Classes nomeadas são mais fáceis de buscar em logs, testar em assertions e filtrar em dashboards de monitoramento.
Confie no mapeamento centralizado de resposta
Deixe HttpServer normalizar erros para JSON de forma consistente, em vez de montar payloads de erro ad hoc em cada handler. Um mapeamento unificado é mais fácil de testar e raciocinar entre equipes, além de reduzir a chance de desvio acidental no formato da resposta.
Nunca exponha detalhes internos de erro para clientes
Exceções desconhecidas são capturadas pela camada de normalização e devolvidas como uma resposta 500 genérica. A mensagem original do erro é registrada em log apenas no lado do servidor. Não sobrescreva esse comportamento para encaminhar stack traces brutos ou mensagens internas, porque isso vaza detalhes de implementação para consumidores externos.