02 de mar. de 2025·8 min de leitura

Erros de restrição do banco na UX: transforme falhas em mensagens claras

Aprenda a transformar erros de restrição do banco em mensagens por campo úteis, mapeando falhas de unique, foreign key e NOT NULL para orientação clara.

Erros de restrição do banco na UX: transforme falhas em mensagens claras

Por que falhas de constraint incomodam tanto os usuários

Quando alguém clica em Salvar, espera um de dois resultados: funcionou, ou dá para consertar rápido o que não deu certo. Muitas vezes aparece um banner genérico como “Request failed” ou “Something went wrong.” O formulário fica igual, nada é destacado e o usuário fica adivinhando.

É aí que entra a importância da UX para erros de constraint do banco. O banco aplica regras que o usuário nunca viu: “esse valor precisa ser único”, “esse registro precisa referenciar um item existente”, “esse campo não pode ficar vazio.” Se o app esconde essas regras atrás de um erro vago, as pessoas se sentem culpadas por algo que não entendem.

Erros genéricos também quebram confiança. As pessoas pensam que o app é instável, então tentam novamente, atualizam a página ou abandonam a tarefa. Em um ambiente de trabalho, elas mandam prints ao suporte que não ajudam, porque o screenshot não traz detalhe útil.

Um exemplo comum: alguém cria um cliente e recebe “Save failed.” Tenta de novo com o mesmo e-mail. Falha de novo. Aí surgem dúvidas: o sistema está duplicando, perdendo dados, ou os dois?

O banco geralmente é a fonte final da verdade, mesmo quando você valida na UI. Ele vê o estado mais recente, incluindo mudanças de outros usuários, jobs em background e integrações. Então falhas de constraint vão acontecer, e isso é normal.

Um bom resultado é simples: transforme uma regra do banco em uma mensagem que aponte para um campo específico e mostre o próximo passo. Por exemplo:

  • “Email já está em uso. Tente outro email ou faça login.”
  • “Escolha uma conta válida. A conta selecionada não existe mais.”
  • “Número de telefone é obrigatório.”

O resto do artigo mostra como fazer essa tradução, para que falhas virem recuperações rápidas, seja escrevendo tudo na mão ou usando uma ferramenta como AppMaster.

Tipos de constraint que você vai encontrar (e o que significam)

A maioria dos momentos de “request failed” vem de um pequeno conjunto de regras do banco. Se você consegue nomear a regra, geralmente dá para transformar em uma mensagem clara no campo certo.

Aqui estão os tipos comuns em linguagem simples:

  • Unique constraint: um valor precisa ser único. Exemplos típicos: email, username, número de fatura ou um ID externo. Quando falha, o usuário não “errou” — houve uma colisão com dados existentes.
  • Foreign key constraint: um registro aponta para outro que deve existir (como order.customer_id). Falha quando o item referenciado foi deletado, nunca existiu ou a UI enviou o ID errado.
  • NOT NULL constraint: um valor obrigatório está faltando no nível do banco. Pode acontecer mesmo se o formulário parece completo (por exemplo, a UI não enviou o campo, ou a API o sobrescreveu).
  • Check constraint: um valor está fora de uma regra permitida, como “quantidade deve ser > 0”, “status deve ser um destes valores” ou “desconto deve estar entre 0 e 100”.

O complicado é que o mesmo problema pode aparecer diferente dependendo do banco e das ferramentas. PostgreSQL pode nomear a constraint (útil), enquanto um ORM pode encapsular em uma exceção genérica (não útil). Até o mesmo unique pode aparecer como “duplicate key”, “unique violation” ou um código específico do fornecedor.

Um exemplo prático: alguém edita um cliente no painel administrativo, clica em Salvar e recebe uma falha. Se a API conseguir dizer ao UI que foi um unique em email, você pode mostrar “Esse email já está em uso” embaixo do campo Email em vez de um toast vago.

Trate cada tipo de constraint como uma pista sobre o que a pessoa pode fazer a seguir: escolher outro valor, selecionar um registro relacionado existente ou preencher o campo obrigatório que falta.

O que uma boa mensagem por campo precisa fazer

Uma falha de constraint é um evento técnico, mas a experiência deve parecer orientação normal. Boa UX para erros de constraint transforma “algo quebrou” em “isso é o que preciso consertar”, sem fazer o usuário adivinhar.

Use linguagem simples. Troque termos de banco como “unique index” ou “foreign key” por algo que uma pessoa diria. “Esse email já está em uso” é muito mais útil que “duplicate key value violates unique constraint.”

Coloque a mensagem onde a ação está. Se o erro pertence claramente a um input, anexe-o a esse campo para o usuário corrigir imediatamente. Se for sobre toda a ação (por exemplo, “você não pode deletar isso porque está sendo usado em outro lugar”), mostre no nível do formulário com um próximo passo claro.

Específico vence cortês. Uma mensagem útil responde duas perguntas: o que precisa mudar e por que foi rejeitado. “Escolha outro nome de usuário” é melhor que “Nome de usuário inválido.” “Selecione um cliente antes de salvar” é melhor que “Dados faltando.”

Tenha cuidado com detalhes sensíveis. Às vezes a mensagem mais “útil” vaza informação. Em tela de login ou recuperação de senha, dizer “Não existe conta para esse email” pode ajudar atacantes. Nesses casos, use uma mensagem mais segura como “Se houver uma conta com este email, você receberá uma mensagem em breve.”

Também planeje mais de um problema ao mesmo tempo. Um único salvar pode falhar em várias constraints. Sua UI deve conseguir mostrar várias mensagens por campo ao mesmo tempo, sem sobrecarregar a tela.

Uma boa mensagem por campo usa palavras simples, aponta para o campo certo (ou é claramente ao nível do formulário), diz ao usuário o que mudar, evita revelar fatos privados e suporta múltiplos erros em uma resposta.

Desenhe um contrato de erro entre API e UI

Boa UX começa com um acordo: quando algo falha, a API diz ao UI exatamente o que aconteceu, e o UI mostra do mesmo jeito sempre. Sem esse contrato, você volta a toasts genéricos que não ajudam ninguém.

Uma forma prática de erro é pequena, mas específica. Deve trazer um código de erro estável, o campo (quando mapeia para um input), uma mensagem humana e detalhes opcionais para logs.

{
  "error": {
    "code": "UNIQUE_VIOLATION",
    "field": "email",
    "message": "That email is already in use.",
    "details": {
      "constraint": "users_email_key",
      "table": "users"
    }
  }
}

O ponto é estabilidade. Não exponha texto bruto do banco para usuários, e não faça o UI parsear strings do Postgres. Códigos devem ser consistentes entre plataformas (web, iOS, Android) e endpoints.

Decida de antemão como representar erros de campo vs erros do formulário. Um erro de campo significa que um input está bloqueado (defina field, mostre a mensagem abaixo do input). Um erro de formulário significa que a ação não pode ser completada mesmo com os campos aparentando estar válidos (deixe field vazio e mostre a mensagem perto do botão Salvar). Se vários campos podem falhar ao mesmo tempo, retorne um array de erros, cada um com seu próprio field e code.

Para manter a renderização consistente, torne suas regras de UI chatas e previsíveis: mostre o primeiro erro no topo como um resumo curto e inline junto ao campo, mantenha mensagens curtas e acionáveis, reaproveite a mesma redação entre fluxos (cadastro, edição de perfil, telas administrativas) e registre details enquanto mostra apenas message ao usuário.

Se você usa AppMaster, trate esse contrato como qualquer outro output de API. Seu backend pode retornar a forma estruturada, e seus apps gerados (web em Vue3 e mobile) podem renderizar com um padrão compartilhado, assim cada falha de constraint parece orientação, não um crash.

Passo a passo: traduzir erros do BD em mensagens por campo

Design constraints with confidence
Model your data in PostgreSQL and keep constraint rules clear as your schema changes.
Build now

Boa UX para erros de constraint começa tratando o banco como juiz final, não a primeira linha de feedback. Usuários não devem ver texto SQL bruto, stack traces ou “request failed.” Eles devem ver qual campo precisa de atenção e o que fazer a seguir.

Um fluxo prático que funciona na maioria das stacks:

  1. Decida onde o erro é capturado. Escolha um único lugar onde erros do banco viram respostas da API (frequentemente a camada de repositório/DAO ou um handler global). Isso evita o caos de “às vezes inline, às vezes toast”.
  2. Classifique a falha. Quando um write falha, detecte a classe: unique, foreign key, NOT NULL ou check. Use códigos do driver quando possível. Evite parsear texto humano, a não ser que não haja alternativa.
  3. Mapeie nomes de constraints para campos do formulário. Constraints são bons identificadores, mas UIs precisam de chaves de campo. Mantenha um lookup simples como users_email_key -> email ou orders_customer_id_fkey -> customerId. Coloque isso perto do código que administra o schema.
  4. Gere uma mensagem segura. Construa texto curto e amigável por classe, não com base na mensagem bruta do banco. Unique -> “Esse valor já está em uso.” FK -> “Escolha um cliente existente.” NOT NULL -> “Este campo é obrigatório.” Check -> “Valor fora do intervalo permitido.”
  5. Retorne erros estruturados e renderize inline. Envie um payload consistente (por exemplo: [{ field, code, message }]). No UI, anexe mensagens aos campos, role e foque o primeiro campo com erro e mantenha qualquer banner global apenas como resumo.

Se você usa AppMaster, aplique a mesma ideia: capture o erro do banco em um único ponto do backend, traduza para um formato previsível de erro por campo e mostre ao lado do input na sua UI web ou mobile. Isso mantém a experiência consistente mesmo quando o modelo de dados evolui.

Um exemplo realista: três salvamentos com falha, três resultados úteis

Essas falhas costumam ser reduzidas a um único toast genérico. Cada uma precisa de uma mensagem diferente, mesmo vindo do banco.

1) Cadastro: email já usado (unique constraint)

Falha bruta (o que aparece nos logs): duplicate key value violates unique constraint "users_email_key"

O que o usuário deve ver: “Esse email já está registrado. Tente entrar ou use outro email.”

Coloque a mensagem ao lado do campo Email e mantenha o formulário preenchido. Se possível, ofereça uma ação secundária como “Entrar”, para que não precise adivinhar o que aconteceu.

2) Criar pedido: cliente ausente (foreign key)

Falha bruta: insert or update on table "orders" violates foreign key constraint "orders_customer_id_fkey"

O que o usuário deve ver: “Escolha um cliente para criar este pedido.”

Isso não parece um “erro” para o usuário — parece contexto faltando. Destaque o seletor Cliente, preserve itens já adicionados ao pedido e, se o cliente foi deletado em outra aba, diga isso claramente: “Esse cliente não existe mais. Escolha outro.”

3) Atualizar perfil: campo obrigatório faltando (NOT NULL)

Falha bruta: null value in column "last_name" violates not-null constraint

O que o usuário deve ver: “Sobrenome é obrigatório.”

Isso é o que um bom tratamento de constraint parece: feedback normal de formulário, não uma falha do sistema.

Para ajudar suporte sem vazar detalhes técnicos ao usuário, mantenha o erro completo nos logs (ou em um painel interno): inclua request ID, user/session ID, nome da constraint (se disponível) e tabela/campo, o payload da API (mas mascarando campos sensíveis), timestamp e o endpoint/ação, além da mensagem mostrada ao usuário.

Erros de chave estrangeira: ajude o usuário a se recuperar

Improve recovery after failures
Keep user input intact, focus the first failing field, and reduce retries and support tickets.
Start a project

Falhas de foreign key geralmente significam que a pessoa escolheu algo que não existe mais, não é mais permitido ou não corresponde às regras atuais. O objetivo não é apenas explicar a falha, mas indicar uma ação clara.

Na maioria das vezes, um erro de foreign key mapeia para um campo: o selector que referencia outro registro (Customer, Project, Assignee). A mensagem deve nomear algo que o usuário reconheça, não o conceito do banco. Evite IDs internos ou nomes de tabela. “Cliente não existe mais” é útil. “FK_orders_customer_id violated (customer_id=42)” não é.

Um padrão sólido de recuperação trata o erro como uma seleção obsoleta. Peça ao usuário para re-selecionar da lista mais recente (atualize o dropdown ou abra o seletor de busca). Se o registro foi deletado ou arquivado, diga isso e oriente a escolher uma alternativa ativa. Se o problema for permissão, informe “Você não tem mais permissão para usar este item” e sugira escolher outro ou contatar um admin. Se criar um registro relacionado for um próximo passo comum, ofereça “Criar novo cliente” em vez de forçar a tentativa repetida.

Registros deletados e arquivados são armadilhas comuns. Se sua UI puder mostrar itens inativos para contexto, marque-os claramente (Arquivado) e impeça a seleção. Isso evita a falha, mas ainda trata o caso quando outro usuário muda dados.

Às vezes uma falha de foreign key deve ser nível do formulário, não do campo. Faça isso quando não for confiável descobrir qual referência causou o erro, quando várias referências são inválidas ou quando o problema real é permissão na ação inteira.

NOT NULL e validação: previna o erro, mas trate quando ocorrer

Stop generic error banners
Centralize error handling in your backend so every screen shows the same helpful messages.
Get started

Falhas NOT NULL são as mais fáceis de prevenir e as mais irritantes quando escapam. Se alguém vê “request failed” depois de deixar um campo obrigatório vazio, o banco está fazendo trabalho da UI. Boa UX significa que a UI bloqueia os casos óbvios, e a API ainda retorna erros claros por campo quando algo passa.

Comece com checagens antecipadas no formulário. Marque campos obrigatórios perto do input, não apenas em um banner genérico. Uma dica curta como “Obrigatório para recibos” é mais útil que um asterisco vermelho sozinho. Se um campo é condicionalmente obrigatório (por exemplo, “Nome da empresa” só quando “Tipo de conta = Empresa”), torne essa regra visível quando relevante.

Validação no UI não basta. Usuários podem burlar com versões antigas do app, rede instável, imports em massa ou automações. Espelhe as mesmas regras na API para não perder uma viagem apenas para falhar no banco.

Mantenha a redação consistente pelo app para que as pessoas aprendam o que cada mensagem significa. Para valores faltando, use “Obrigatório.” Para limites de comprimento, use “Muito longo (máx 50 caracteres).” Para formato, use “Formato inválido (use [email protected]).” Para tipos, use “Deve ser um número.”

Atualizações parciais são onde NOT NULL complica. Um PATCH que omite um campo obrigatório não deveria falhar se o valor existente já estiver presente, mas deve falhar se o cliente explicitamente setar para null ou vazio. Decida essa regra uma vez, documente e aplique consistentemente.

Uma abordagem prática é validar em três camadas: regras no cliente, validação na requisição da API e um último filtro que captura um erro NOT NULL do banco e mapeia para o campo correto.

Erros comuns que voltam ao “request failed”

A maneira mais rápida de arruinar o tratamento de constraints é fazer todo o trabalho no banco e depois esconder o resultado atrás de um toast genérico. Usuários não ligam que uma constraint foi acionada. Eles querem saber o que consertar, onde e se os dados estão seguros.

Um deslize comum é mostrar texto bruto do banco. Mensagens como duplicate key value violates unique constraint parecem um crash, mesmo quando o app pode se recuperar. Elas também geram tickets de suporte porque os usuários copiam o texto assustador em vez de corrigir um campo.

Outra armadilha é confiar em correspondência de strings. Funciona até você trocar de driver, atualizar o Postgres ou renomear uma constraint. Então seu mapeamento “email já usado” para de funcionar silenciosamente e você volta ao “request failed.” Prefira códigos de erro estáveis e inclua o nome do campo que a UI entende.

Mudanças no schema quebram mapeamentos mais vezes do que se espera. Um rename de email para primary_email pode transformar uma mensagem clara em dados sem onde mostrar. Faça o mapeamento parte do mesmo conjunto de mudanças da migração e falhe alto nos testes se uma chave de campo for desconhecida.

Um grande assassino de UX é transformar cada falha de constraint em HTTP 500 sem corpo. Isso diz ao UI “é culpa do servidor”, então ele não pode mostrar dicas por campo. A maioria das falhas de constraint é corrigível pelo usuário, então retorne uma resposta no estilo de validação com detalhes.

Alguns padrões para ficar de olho:

  • Mensagens de email único que confirmam existência de conta (use linguagem neutra em cadastros)
  • Tratar “um erro por vez” e esconder o segundo campo quebrado
  • Formulários multi-etapa que perdem erros ao navegar
  • Retries que submetem valores obsoletos e sobrescrevem a mensagem correta do campo
  • Logs que perdem o nome da constraint ou o código, dificultando rastrear bugs

Por exemplo, se um formulário de cadastro diz “Email já existe”, você pode estar vazando existência de conta. Uma mensagem mais segura é “Verifique seu email ou tente entrar”, enquanto ainda anexa o erro ao campo email.

Checklist rápido antes de lançar

One error format everywhere
Create a consistent error contract once and reuse it across web and native mobile apps.
Start building

Antes de liberar, verifique os pequenos detalhes que decidem se uma falha de constraint parece uma ajuda ou um beco sem saída.

Resposta da API: o UI pode agir com ela?

Assegure que cada falha no estilo validação retorna estrutura suficiente para apontar a um input específico. Para cada erro, retorne field, um code estável e uma message humana. Cubra os casos comuns do banco (unique, foreign key, NOT NULL, check). Guarde detalhes técnicos para logs, não para usuários.

Comportamento do UI: ajuda a pessoa a se recuperar?

Mesmo a melhor mensagem fica ruim se o formulário atrapalhar. Foque o primeiro campo com erro e role ele para a vista se preciso. Preserve o que o usuário já digitou (especialmente após erros em vários campos). Mostre erros no nível do campo primeiro, com um resumo curto só quando fizer sentido.

Logs e testes: você pega regressões?

O tratamento de constraints costuma quebrar silenciosamente quando schemas mudam, então trate como uma feature a manter. Logue o erro do BD internamente (nome da constraint, tabela, operação, request ID), mas nunca mostre ao usuário. Adicione testes para pelo menos um exemplo por tipo de constraint e verifique que seu mapeamento permanece estável mesmo se a redação do banco mudar.

Próximos passos: torne consistente em todo o app

A maioria das equipes corrige erros de constraint tela a tela. Isso ajuda, mas usuários notam as lacunas: um formulário mostra mensagem clara, outro ainda diz “request failed.” Consistência é o que transforma isso de remendo em padrão.

Comece onde dói. Puxe uma semana de logs ou tickets de suporte e escolha as constraints que aparecem com mais frequência. Esses “maiores ofensores” devem ser os primeiros a ganhar mensagens amigáveis por campo.

Trate a tradução de erros como uma pequena feature de produto. Tenha um mapeamento compartilhado usado por todo o app: nome da constraint (ou código) -> nome do campo -> mensagem -> dica de recuperação. Mantenha mensagens simples e dicas acionáveis.

Um plano de rollout leve para um ciclo de produto ocupado:

  • Identifique as 5 constraints que mais aparecem e escreva a mensagem exata para cada uma.
  • Adicione uma tabela de mapeamento e use-a em todos os endpoints que salvam dados.
  • Padronize como os formulários renderizam erros (mesma posição, mesmo tom, mesmo comportamento de foco).
  • Reveja as mensagens com um colega não técnico e pergunte: “O que você faria a seguir?”
  • Adicione um teste por formulário que confirme o campo correto é destacado e a mensagem é legível.

Se quiser aplicar esse comportamento sem escrever cada tela manualmente, AppMaster (appmaster.io) suporta APIs de backend mais UIs web e mobile geradas. Isso facilita reaplicar um formato de erro estruturado em vários clientes, para que feedback por campo permaneça consistente conforme seu modelo de dados muda.

Escreva também uma nota curta de “estilo de mensagens de erro” para o time. Mantenha simples: quais palavras evitar (termos de banco) e o que toda mensagem deve incluir (o que aconteceu e o que fazer a seguir).

FAQ

Why do database constraint errors feel so frustrating to users?

Trate isso como um feedback normal de formulário, não como uma falha do sistema. Mostre uma mensagem curta perto do campo exato que precisa ser alterado, mantenha os dados já preenchidos e explique o próximo passo em linguagem simples.

What’s the difference between a field-level error and a generic “request failed” message?

Um erro ao nível do campo aponta para um único input e diz ao usuário o que corrigir ali mesmo, por exemplo “Email já está em uso.” Um erro genérico força o usuário a adivinhar, tentar novamente ou abrir um chamado de suporte, porque não indica o que alterar.

How do I reliably detect which constraint failed?

Use códigos de erro estáveis do driver do banco quando possível e mapeie-os para tipos amigáveis ao usuário (único, chave estrangeira, obrigatório, intervalo). Evite parsear texto bruto do banco, pois muda entre drivers e versões.

How do I map a constraint name to the correct form field?

Mantenha um mapeamento simples do nome da constraint para a chave do campo no UI no backend, próximo ao lugar que gerencia o esquema. Por exemplo: um unique em email -> email, para que a UI destaque o input certo sem adivinhar.

What should I say for a unique constraint error (like duplicate email)?

Padronize para algo como “Esse valor já está em uso” e acrescente uma ação clara, como “Tente outro” ou “Entrar”, dependendo do fluxo. Em cadastros e recuperação de senha, prefira linguagem neutra para não confirmar existência de conta.

How should I handle foreign key errors without confusing people?

Explique como uma seleção obsoleta ou inválida que o usuário reconhece, por exemplo “Esse cliente não existe mais. Escolha outro.” Se necessário, ofereça no UI a opção de criar o registro relacionado em vez de forçar várias tentativas.

If my UI validates required fields, why do NOT NULL errors still happen?

Marque campos obrigatórios no UI e valide antes do envio, mas mantenha o banco como uma camada de segurança. Quando ocorrer, mostre “Obrigatório” no campo correspondente e preserve o restante do formulário.

How do I handle multiple constraint errors from one Save?

Retorne um array de erros, cada um com a chave do campo, um código estável e uma mensagem curta, para que a UI mostre todos de uma vez. No cliente, foque o primeiro campo com erro, mas mantenha as outras mensagens visíveis.

What should an API error response include so the UI can render it correctly?

Use um payload consistente que separe o que o usuário vê do que você registra: uma mensagem amigável ao usuário e, internamente, detalhes como nome da constraint e request ID. Nunca exponha erros SQL crus ao usuário.

How can I keep constraint error handling consistent across web and mobile apps?

Centralize a tradução em um ponto do backend, retorne um formato previsível de erro e renderize do mesmo jeito em todos os clientes. Com AppMaster, você pode aplicar um contrato estruturado de erro em APIs geradas e UIs web/mobile, mantendo mensagens consistentes conforme o modelo de dados muda.

Fácil de começar
Criar algo espantoso

Experimente o AppMaster com plano gratuito.
Quando estiver pronto, você poderá escolher a assinatura adequada.

Comece