O que é um layout de exceção
Quando uma pessoa executa um comando e algo dá errado, o que ela vê no chat é o layout. Essa é a resposta visual que o Spraxium envia em nome do erro: uma mensagem em texto, um embed com título e descrição, uma resposta efêmera ou qualquer combinação disso. A classe de exceção é o sinal interno que informa o que falhou e carrega os dados de contexto. O layout é o template que consome esse contexto e o transforma na mensagem real do Discord que a pessoa vai ler.
Essa separação permite que o código de domínio continue focado nas regras de negócio e nunca precise saber como o erro aparece para a pessoa usuária. Um guard que nega acesso lança uma PermissionDeniedException. Ele não monta um embed, não compõe uma string de mensagem e não decide se a resposta deve ser efêmera. Tudo isso fica no layout, que pode ser alterado ou sobrescrito de forma independente, sem tocar em nenhuma lógica de domínio.
O método build retorna um objeto ExceptionLayoutPayload. Todos os campos são opcionais e podem ser combinados livremente. Um layout pode retornar apenas conteúdo em texto, apenas embeds ou uma combinação com componentes interativos.
| Campo | Type | Finalidade |
|---|---|---|
| content | string | Resposta em texto simples enviada como corpo da mensagem |
| embeds | List of EmbedBuilder entries | Cards de embed com cor, título, descrição e campos |
| components | List of ActionRowBuilder entries | Componentes interativos, como botões ou select menus |
| ephemeral | boolean | Define se a resposta fica visível apenas para quem acionou a ação; o padrão é true |
| flags | MessageFlags | Flags de mensagem do Discord em baixo nível para controle avançado da resposta |
Ordem de precedência no runtime
O Spraxium resolve layouts em uma ordem fixa, e conhecer essa ordem é essencial para evitar confusão quando personalizações locais e configuração global coexistem na mesma aplicação. Cada nível da cadeia de precedência pode sobrescrever o nível abaixo, o que dá um controle bem granular sobre como os erros são apresentados sem criar ambiguidade sobre qual layout realmente será renderizado.
- Override por instância no objeto de exceção
- Mapeamento na configuração que corresponde ao nome da classe de exceção
- Decorator no nível da classe com
@WithLayout() - A entrada padrão no mapa de layouts da configuração
- O DefaultExceptionLayout embutido no framework
Consequência prática
Se você mapear uma classe no mapa de layouts da configuração, esse mapeamento terá prioridade sobre o decorator no nível da classe, permitindo que a configuração da aplicação sobrescreva defaults da biblioteca quando necessário.
Quando usar @WithLayout
Use o decorator quando a própria classe de exceção precisar carregar um layout padrão semântico como parte da sua definição. Essa abordagem funciona bem em pacotes compartilhados ou código de biblioteca, em que você quer que a classe já venha com um fallback razoável sem depender da configuração da aplicação consumidora. Equipes que preferem convenções centradas em classes também se beneficiam desse padrão, porque ele reduz a chance de esquecer de adicionar um mapeamento de layout ao criar uma nova classe de exceção.
| Cenário | Por que o decorator faz sentido | Trade-off |
|---|---|---|
| Pacote compartilhado de exceções | A classe já entrega um layout padrão opinativo | A aplicação ainda pode sobrescrevê-lo na configuração |
| Convenções de equipe centradas em classes | Reduz a chance de esquecer mapeamentos | Menos política centralizada em um único arquivo |
Quando mapear na configuração
Faça o mapeamento em spraxium.config.ts quando a política de layout for guiada por decisões de produto, como branding, tom de linguagem ou estratégia específica por ambiente. Em geral, essa é a melhor opção para governança no nível da aplicação, porque centraliza toda a lógica de apresentação de erros em um só lugar e facilita a gestão de mudanças, já que você pode trocar a política de layout sem modificar nenhuma classe de exceção de domínio.
| Cenário | Por que o mapeamento em config faz sentido | Trade-off |
|---|---|---|
| UX de erros alinhada à marca | Centraliza estilo, tom e política de fallback | Exige disciplina no mapeamento |
| Política diferente por ambiente | Pode trocar o comportamento sem tocar nas classes | A config pode crescer demais sem convenções |
Quando usar override por instância
Use o override por instância apenas em casos contextuais, quando a mesma classe de exceção precisa ser renderizada de forma diferente conforme as condições de runtime. Exemplos típicos incluem comunicação temporária durante incidentes, migrações em andamento ou experimentos com feature flags em que a resposta de erro precisa mudar sem alterar definições de classe ou arquivos de configuração. Como esse nível tem a maior prioridade na cadeia de resolução, trate-o como um caminho de exceção intencional e documente por que ele existe, para evitar comportamentos implícitos difíceis de rastrear durante a depuração.
Exemplo completo com embed personalizado
O exemplo a seguir mostra como criar uma classe de exceção com um layout no nível do decorator e depois sobrescrevê-lo na configuração da aplicação. O mapeamento na configuração tem prioridade sobre o decorator, o que significa que o layout alinhado à identidade da equipe é renderizado em produção, enquanto o decorator funciona como fallback para qualquer consumidor que não configure um mapeamento específico.
import { WithLayout } from '@spraxium/common';
import { defineExceptionLayout, SpraxiumException } from '@spraxium/core';
import { EmbedBuilder } from 'discord.js';
const PolicyLayout = defineExceptionLayout((exception) => ({
embeds: [
new EmbedBuilder()
.setColor(0xffb020)
.setTitle('Action blocked by policy')
.setDescription(String(exception.props.reason ?? 'Reason not provided')),
],
ephemeral: true,
}));
@WithLayout(PolicyLayout)
export class PolicyException extends SpraxiumException {
constructor(props: { reason: string }) {
super({
code: 'POLICY_BLOCKED',
message: 'Action blocked: {{reason}}',
props,
});
}
}E na configuração da aplicação:
exceptions: {
logUnhandled: true,
layouts: {
default: AppDefaultLayout,
PolicyException: TeamBrandPolicyLayout,
},
}Regra de decisão recomendada
Ao decidir qual abordagem usar, siga esta ordem de preferência para manter a arquitetura de tratamento de erros limpa e sustentável. Use o mapa de layouts da configuração como política principal da aplicação, use o decorator como fallback semântico no nível da classe para bibliotecas e pacotes compartilhados e reserve overrides por instância exclusivamente para comportamentos realmente contextuais que não possam ser expressos pelos outros dois mecanismos.