UX de errores por restricciones de base de datos: convierte fallos en mensajes claros
Aprende cómo la UX de errores por restricciones de la base de datos puede convertirse en mensajes útiles en los campos, mapeando fallos unique, foreign key y NOT NULL en tu aplicación.

Por qué las fallas de restricciones resultan tan malas para los usuarios
Cuando alguien pulsa Guardar, espera uno de dos resultados: funcionó, o puede arreglar rápido lo que falló. Demasiadas veces recibe un banner genérico como “Request failed” o “Something went wrong.” El formulario permanece igual, nada está resaltado y la persona se queda adivinando.
Esa brecha es la razón por la que la UX de errores por restricciones de base de datos importa. La base de datos está aplicando reglas que el usuario nunca vio: “este valor debe ser único”, “este registro debe referenciar a un elemento existente”, “este campo no puede estar vacío”. Si la app oculta esas reglas detrás de un error vago, la gente se siente culpada por un problema que no entiende.
Los errores genéricos también rompen la confianza. Las personas asumen que la app es inestable, así que reintentan, actualizan o abandonan la tarea. En entornos de trabajo, envían capturas de pantalla a soporte que no ayudan, porque la captura no contiene detalles útiles.
Un ejemplo común: alguien crea un cliente y recibe “Save failed.” Lo intenta otra vez con el mismo correo y vuelve a fallar. Ahora se pregunta si el sistema está duplicando datos, perdiéndolos, o ambas cosas.
La base de datos suele ser la fuente de la verdad, incluso cuando validas en la UI. Ve el estado más reciente, incluidas los cambios de otros usuarios, jobs en segundo plano e integraciones. Por eso los fallos por restricciones ocurrirán, y eso es normal.
Un buen resultado es simple: convierte una regla de la base de datos en un mensaje que señale un campo específico y diga qué hacer después. Por ejemplo:
- “El correo ya está en uso. Prueba con otro correo o inicia sesión.”
- “Elige una cuenta válida. La cuenta seleccionada ya no existe.”
- “Se requiere número de teléfono.”
El resto del artículo trata sobre hacer esa traducción, para que los fallos se conviertan en recuperaciones rápidas, tanto si programas la pila a mano como si construyes con una herramienta como AppMaster.
Los tipos de restricciones con los que te encontrarás (y qué significan)
La mayoría de los momentos de “request failed” provienen de un pequeño conjunto de reglas de la base de datos. Si puedes nombrar la regla, normalmente puedes convertirla en un mensaje claro junto al campo correcto.
Aquí están los tipos comunes en lenguaje llano:
- Unique constraint: Un valor debe ser único. Ejemplos típicos son correo, nombre de usuario, número de factura o un ID externo. Cuando falla, el usuario no “hizo algo mal”, colisionó con datos existentes.
- Foreign key constraint: Un registro apunta a otro que debe existir (como
order.customer_id). Falla cuando lo referenciado fue eliminado, nunca existió o la UI envió el ID equivocado. - NOT NULL constraint: Falta un valor requerido a nivel de base de datos. Puede pasar aunque el formulario parezca completo (por ejemplo, la UI no envió un campo, o la API lo sobreescribió).
- Check constraint: Un valor está fuera de una regla permitida, como “quantity debe ser > 0”, “status debe ser uno de estos valores” o “discount debe estar entre 0 y 100”.
La parte complicada es que el mismo problema real puede lucir distinto según la base de datos y las herramientas. Postgres puede nombrar la restricción (útil), mientras que un ORM puede envolverla en una excepción genérica (poco útil). Incluso la misma restricción única puede aparecer como “duplicate key”, “unique violation” o un código específico del proveedor.
Un ejemplo práctico: alguien edita un cliente en un panel de administración, pulsa Guardar y ocurre un fallo. Si la API puede indicar a la UI que fue una restricción única en email, puedes mostrar “Este correo ya está en uso” bajo el campo Email en lugar de un toast vago.
Trata cada tipo de restricción como una pista sobre lo que la persona puede hacer a continuación: elegir otro valor, elegir un registro relacionado existente o completar el campo requerido que falta.
Qué debe hacer un buen mensaje a nivel de campo
Un fallo por restricción de base de datos es un evento técnico, pero la experiencia debería sentirse como orientación normal. Una buena UX convierte “algo falló” en “esto es lo que hay que corregir”, sin obligar al usuario a adivinar.
Usa lenguaje llano. Sustituye términos de base de datos como “unique index” o “foreign key” por algo que una persona diría. “Ese correo ya está en uso” es mucho más útil que “duplicate key value violates unique constraint.”
Pon el mensaje donde está la acción. Si el error pertenece claramente a un input, adjúntalo a ese campo para que el usuario pueda corregirlo de inmediato. Si se trata de toda la acción (por ejemplo, “no puedes eliminar esto porque se usa en otro lugar”), muéstralo a nivel de formulario con un siguiente paso claro.
Ser específico gana a ser cortés. Un mensaje útil responde a dos preguntas: qué hay que cambiar y por qué se rechazó. “Elige otro nombre de usuario” es mejor que “Nombre de usuario inválido.” “Selecciona un cliente antes de guardar” gana a “Faltan datos.”
Ten cuidado con los detalles sensibles. A veces el mensaje más “útil” filtra información. En una pantalla de login o restablecimiento, decir “No existe una cuenta para este correo” puede ayudar a atacantes. En esos casos, usa un mensaje más seguro como “Si existe una cuenta con este correo, recibirás un mensaje pronto.”
También planifica más de un problema a la vez. Un único Guardar puede fallar por varias restricciones. Tu UI debería poder mostrar varios mensajes de campo juntos, sin abrumar la pantalla.
Un mensaje sólido a nivel de campo usa palabras simples, señala el campo correcto (o es claramente a nivel de formulario), dice qué cambiar, evita revelar hechos privados sobre cuentas o registros y soporta múltiples errores en una sola respuesta.
Diseña un contrato de errores entre API y UI
Una buena UX empieza con un acuerdo: cuando algo falla, la API le dice a la UI exactamente qué pasó y la UI lo muestra igual siempre. Sin ese contrato, vuelves al toast genérico que no ayuda a nadie.
Una forma de error práctica es pequeña pero específica. Debe llevar un código de error estable, el campo (cuando se mapea a un input), un mensaje humano y detalles opcionales para logging.
{
"error": {
"code": "UNIQUE_VIOLATION",
"field": "email",
"message": "That email is already in use.",
"details": {
"constraint": "users_email_key",
"table": "users"
}
}
}
La clave es la estabilidad. No expongas texto crudo de la base a los usuarios, y no hagas que la UI parsee cadenas de error de Postgres. Los códigos deben ser consistentes entre plataformas (web, iOS, Android) y entre endpoints.
Decide desde el principio cómo representas errores de campo frente a errores a nivel de formulario. Un error de campo significa que un input está bloqueado (pon field, muestra el mensaje bajo el input). Un error a nivel de formulario significa que la acción no puede completarse aunque los campos parezcan válidos (deja field vacío, muestra el mensaje cerca del botón Guardar). Si múltiples campos pueden fallar a la vez, devuelve un array de errores, cada uno con su propio field y code.
Para mantener el renderizado consistente, haz que tus reglas de UI sean aburridas y predecibles: muestra el primer error cerca de la parte superior como un resumen corto y en línea junto al campo, mantén mensajes breves y accionables, reutiliza el mismo texto en distintos flujos (registro, edición de perfil, pantallas de administración) y registra details mientras muestras solo message.
Si construyes con AppMaster, trata este contrato como cualquier otra salida de API. Tu backend puede devolver la forma estructurada y las apps generadas (web en Vue3 y móviles) pueden renderizarlo con un patrón compartido, de modo que cada fallo por restricción se sienta como orientación, no como un bloqueo.
Paso a paso: traducir errores de BD a mensajes de campo
Una buena UX comienza tratando a la base de datos como el juez final, no como la primera línea de feedback. Los usuarios no deberían ver texto SQL crudo, trazas de pila o “request failed” vago. Deberían ver qué campo necesita atención y qué hacer a continuación.
Un flujo práctico que funciona en la mayoría de pilas:
- Decide dónde se captura el error. Elige un lugar donde los errores de base de datos se conviertan en respuestas de API (a menudo la capa de repositorio/DAO o un manejador global de errores). Esto evita el caos de “a veces inline, a veces toast”.
- Clasifica la falla. Cuando una escritura falla, detecta la clase: restricción única, clave foránea, NOT NULL o check. Usa códigos de driver cuando sea posible. Evita parsear texto humano salvo que no tengas alternativa.
- Mapea nombres de restricciones a campos de formulario. Las restricciones son buenos identificadores, pero las UIs necesitan claves de campo. Mantén un lookup simple como
users_email_key -> emailoorders_customer_id_fkey -> customerId. Ponlo cerca del código que controla el esquema. - Genera un mensaje seguro. Construye texto corto y amigable por clase, no desde el mensaje crudo de la BD. Unique -> “Este valor ya está en uso.” FK -> “Elige un cliente existente.” NOT NULL -> “Este campo es obligatorio.” Check -> “Valor fuera del rango permitido.”
- Devuelve errores estructurados y muéstralos en línea. Envía una carga coherente (por ejemplo:
[{ field, code, message }]). En la UI, adjunta mensajes a los campos, desplázate y enfoca el primer campo con error, y deja cualquier banner global como resumen solamente.
Si construyes con AppMaster, aplica la misma idea: captura el error en un único punto del backend, tradúcelo a un formato de error de campo predecible y muéstralo junto al input en tu UI web o móvil. Así la experiencia se mantiene consistente incluso si el modelo de datos evoluciona.
Un ejemplo realista: tres guardados fallidos, tres resultados útiles
A menudo estos fallos se colapsan en un único toast genérico. Cada uno necesita un mensaje diferente, aunque todos vengan de la base de datos.
1) Registro: correo ya usado (restricción única)
Raw failure (lo que verías en logs): duplicate key value violates unique constraint "users_email_key"
Qué debe ver el usuario: “Ese correo ya está registrado. Prueba a iniciar sesión o usa otro correo.”
Pon el mensaje junto al campo Email y deja el formulario rellenado. Si puedes, ofrece una acción secundaria como “Iniciar sesión”, para que no tengan que adivinar qué pasó.
2) Crear pedido: falta cliente (clave foránea)
Raw failure: insert or update on table "orders" violates foreign key constraint "orders_customer_id_fkey"
Qué debe ver el usuario: “Selecciona un cliente para crear este pedido.”
Esto no se siente como un “error” para el usuario, sino como contexto faltante. Resalta el selector Cliente, conserva las líneas de pedido que ya añadieron y, si el cliente fue borrado en otra pestaña, dilo claramente: “Ese cliente ya no existe. Elige otro.”
3) Actualizar perfil: falta campo requerido (NOT NULL)
Raw failure: null value in column "last_name" violates not-null constraint
Qué debe ver el usuario: “Apellido es obligatorio.”
Así es como debe manejarse una restricción: retroalimentación normal de formulario, no un fallo del sistema.
Para ayudar a soporte sin filtrar detalles técnicos, conserva el error completo en logs (o en un panel interno): incluye un request ID y user/session ID, el nombre de la restricción (si está disponible) y la tabla/campo, la carga de la API (enmascara campos sensibles), la marca de tiempo, el endpoint/acción y el mensaje visible al usuario que se mostró.
Errores de clave foránea: ayuda al usuario a recuperarse
Las fallas por clave foránea suelen significar que la persona eligió algo que ya no existe, ya no está permitido o no coincide con las reglas actuales. El objetivo no es solo explicar el fallo, sino dar un siguiente paso claro.
La mayoría de las veces, un error de clave foránea se mapea a un campo: el selector que referencia otro registro (Cliente, Proyecto, Asignado). El mensaje debe nombrar la entidad que el usuario reconoce, no el concepto de base de datos. Evita IDs internas o nombres de tabla. “El cliente ya no existe” es útil. “FK_orders_customer_id violated (customer_id=42)” no lo es.
Un patrón de recuperación sólido trata el error como una selección obsoleta. Invita al usuario a re-seleccionar desde la lista más reciente (refresca el dropdown o abre el picker). Si el registro fue eliminado o archivado, dilo y guía hacia una alternativa activa. Si al usuario le faltan permisos, indica “Ya no tienes permiso para usar este elemento” y pídeles elegir otro o contactar a un admin. Si crear un registro relacionado es un paso normal, ofrece “Crear nuevo cliente” en lugar de forzar el reintento.
Los registros eliminados o archivados son una trampa común. Si tu UI puede mostrar elementos inactivos por contexto, etiquétalos claramente (Archivado) e impide su selección. Eso previene el fallo, pero aún maneja el caso cuando otro usuario cambia datos.
A veces un fallo de clave foránea debe ser a nivel de formulario, no de campo. Hazlo así cuando no puedas decir con fiabilidad qué referencia causó el error, cuando múltiples referencias son inválidas o cuando el verdadero problema son permisos en toda la acción.
NOT NULL y validación: prevenir el error, pero aún manejarlo
Los fallos NOT NULL son los más fáciles de prevenir y los más molestos cuando se cuelan. Si alguien ve “request failed” tras dejar un campo obligatorio vacío, la base de datos está haciendo trabajo de la UI. Buena UX significa que la UI bloquea los casos obvios, y la API sigue devolviendo errores claros a nivel de campo cuando algo se pasa por alto.
Empieza con validaciones tempranas en el formulario. Marca los campos obligatorios cerca del input, no en un banner genérico. Un breve indicador como “Requerido para recibos” es más útil que solo un asterisco rojo. Si un campo es condicionalmente obligatorio (por ejemplo, “Nombre de la empresa” solo cuando “Tipo de cuenta = Empresa”), haz visible esa regla en el momento en que se vuelva relevante.
La validación en la UI no basta. Los usuarios pueden evitarla con versiones antiguas de la app, redes inestables, importaciones masivas o automatizaciones. Replica las mismas reglas en la API para no desperdiciar una ida y vuelta solo para fallar en la base.
Mantén la redacción consistente en la app para que la gente aprenda qué significa cada mensaje. Para valores faltantes, usa “Requerido.” Para límites de longitud, usa “Demasiado largo (máx. 50 caracteres).” Para formatos, “Formato inválido (usa [email protected]).” Para tipos, “Debe ser un número.”
Las actualizaciones parciales complican NOT NULL. Un PATCH que omite un campo requerido no debería fallar si el valor existente ya está presente, pero sí debería fallar si el cliente lo establece explícitamente a null o a un valor vacío. Decide esta regla una vez, documéntala y aplícala con consistencia.
Un enfoque práctico es validar en tres capas: reglas en el formulario cliente, validación de la solicitud en la API y una red de seguridad final que capture un error NOT NULL de la base y lo mapee al campo correcto.
Errores comunes que devuelven a “request failed”
La forma más rápida de arruinar el manejo de restricciones es hacer todo el trabajo duro en la base de datos y luego ocultar el resultado tras un toast genérico. A los usuarios no les importa que haya saltado una restricción; quieren saber qué arreglar, dónde y si sus datos están a salvo.
Una falla común es mostrar texto crudo de la base de datos. Mensajes como duplicate key value violates unique constraint parecen un crash, incluso cuando la app puede recuperarse. También generan tickets de soporte porque los usuarios copian texto alarmante en lugar de corregir un campo.
Otra trampa es depender de emparejamientos por cadena. Funciona hasta que cambias de driver, actualizas Postgres o renombras una restricción. Entonces tu mapeo “correo ya usado” deja de funcionar en silencio y vuelves a “request failed.” Prefiere códigos de error estables e incluye la clave del campo que entiende tu UI.
Los cambios de esquema rompen el mapeo de campos más seguido de lo que cree la gente. Un rename de email a primary_email puede convertir un mensaje claro en datos sin lugar donde mostrarse. Haz que el mapeo sea parte del mismo conjunto de cambios que la migración y falla ruidosamente en tests si una clave de campo es desconocida.
Un gran asesino de UX es convertir cada fallo de restricción en un HTTP 500 sin cuerpo. Eso le dice a la UI “esto es culpa del servidor”, así que no puede mostrar pistas de campo. La mayoría de fallos por restricciones se pueden corregir por el usuario, así que devuelve una respuesta estilo validación con detalles.
Algunos patrones a vigilar:
- Mensajes de email único que confirman que existe una cuenta (usa redacción neutral en flujos de registro)
- Manejar “un error a la vez” y ocultar el segundo campo roto
- Formularios multi-paso que pierden errores al navegar atrás/adelante
- Reintentos que envían valores obsoletos y sobreescriben el mensaje correcto del campo
- Logging que pierde el nombre de la restricción o el código de error, dificultando el rastreo de bugs
Por ejemplo, si un formulario de registro dice “El correo ya existe”, podrías estar filtrando la existencia de una cuenta. Un mensaje más seguro sería “Revisa tu correo o intenta iniciar sesión”, mientras aún se adjunta el error al campo email.
Lista rápida antes de lanzar
Antes de lanzar, comprueba los pequeños detalles que deciden si un fallo por restricción se siente como una ayuda o un callejón sin salida.
Respuesta API: ¿la UI puede actuar sobre ella?
Asegúrate de que cada fallo estilo validación devuelva suficiente estructura para apuntar a un input específico. Para cada error, devuelve un field, un code estable y un message humano. Cubre los casos comunes de base de datos (unique, foreign key, NOT NULL, check). Deja los detalles técnicos para logs, no para usuarios.
Comportamiento de la UI: ¿ayuda a la persona a recuperarse?
Incluso un mensaje perfecto se siente mal si el formulario pelea con el usuario. Enfoca el primer campo con error y desplázalo a la vista si es necesario. Conserva lo que el usuario ya escribió (especialmente tras errores en varios campos). Muestra errores a nivel de campo primero y usa un resumen corto solo cuando ayude.
Logging y tests: ¿capturas regresiones?
El manejo de restricciones suele romperse en silencio cuando cambian esquemas, así que trátalo como una funcionalidad mantenida. Registra el error de BD internamente (nombre de restricción, tabla, operación, request ID), pero nunca lo muestres directamente. Añade tests para al menos un ejemplo por tipo de restricción y verifica que tu mapeo se mantenga estable aunque cambie el texto exacto de la base de datos.
Próximos pasos: hazlo consistente en toda tu app
La mayoría de los equipos arreglan errores de restricciones una pantalla a la vez. Eso ayuda, pero los usuarios notan los huecos: un formulario muestra un mensaje claro y otro sigue diciendo “request failed”. La consistencia es lo que convierte esto de un parche a un patrón.
Empieza donde más duele. Revisa una semana de logs o tickets de soporte y elige las restricciones que aparecen una y otra vez. Esos “principales ofensores” deben ser los primeros en recibir mensajes amigables a nivel de campo.
Trata la traducción de errores como una pequeña característica de producto. Mantén un mapeo compartido que use toda la app: nombre de restricción (o código) -> nombre del campo -> mensaje -> pista de recuperación. Mantén mensajes simples y la pista accionable.
Un plan de despliegue ligero que encaja en un ciclo de producto ocupado:
- Identifica las 5 restricciones que más golpean a los usuarios y escribe el mensaje exacto que quieres mostrar.
- Añade una tabla de mapeo y úsala en todos los endpoints que guardan datos.
- Estandariza cómo los formularios renderizan errores (misma colocación, mismo tono, mismo comportamiento de foco).
- Revisa los mensajes con un compañero no técnico y pregunta: “¿Qué harías ahora?”
- Añade un test por formulario que verifique que se resalta el campo correcto y que el mensaje es legible.
Si quieres construir este comportamiento sin escribir a mano cada pantalla, AppMaster (appmaster.io) soporta APIs de backend más apps web y nativas generadas. Eso facilita reutilizar un formato de error estructurado entre clientes, de modo que la retroalimentación a nivel de campo se mantenga coherente a medida que cambia tu modelo de datos.
Escribe también una breve nota de “estilo de mensajes de error” para tu equipo. Mantenla simple: qué palabras evitar (términos de base de datos) y qué debe incluir cada mensaje (qué pasó y qué hacer después).
FAQ
Trátalo como retroalimentación normal de formulario, no como un fallo del sistema. Muestra un mensaje corto junto al campo exacto que necesita cambios, conserva lo que el usuario ya escribió y explica el siguiente paso en lenguaje claro.
Un error a nivel de campo señala un input concreto y dice qué arreglar ahí mismo, por ejemplo “El correo ya está en uso.” Un error genérico obliga a adivinar, a reintentos y a enviar mensajes a soporte porque no indica qué cambiar.
Usa códigos de error estables del driver de la base de datos cuando sea posible y mápatalos a tipos amigables para el usuario: único, clave foránea, requerido o reglas de rango. Evita analizar el texto crudo de la base porque cambia entre drivers y versiones.
Mantén un mapeo simple desde el nombre de la restricción hasta la clave del campo de la UI en el backend, cerca del código que controla el esquema. Por ejemplo, mapea una restricción única de email a la clave email para que la UI pueda resaltar el input correcto sin adivinar.
Por defecto, muestra “Este valor ya está en uso” y añade una acción clara como “Prueba con otro” o “Inicia sesión”, según el flujo. En registros o restablecimiento de contraseña usa un texto neutral para no confirmar si existe una cuenta.
Explícalo como una selección obsoleta o inválida que el usuario reconoce, por ejemplo “Ese cliente ya no existe. Elige otro.” Si el usuario no puede recuperarse, ofrece la opción de crear el registro relacionado o una ruta guiada en la UI en lugar de forzar varios intentos.
Marca los campos obligatorios en la UI y valida antes de enviar, pero sigue tratando el fallo en la base como una red de seguridad. Cuando ocurra, muestra “Obligatorio” en el campo y conserva el resto del formulario intacto.
Devuelve un array de errores, cada uno con la clave del campo, un código estable y un mensaje corto, para que la UI los muestre todos a la vez. En el cliente enfoca el primer campo con error pero deja visibles los demás para evitar el bucle de “un error a la vez”.
Usa una respuesta consistente que separe lo que ve el usuario de lo que registras, por ejemplo un mensaje para el usuario y detalles internos como nombre de la restricción e ID de la petición. Nunca expongas errores SQL crudos ni obligues a la UI a parsear cadenas de la base de datos.
Centraliza la traducción en un único punto del backend, devuelve una forma predecible de error y renderízala igual en todos los formularios. Con AppMaster puedes aplicar el mismo contrato estructurado de errores en APIs y en UIs web/móviles generadas, lo que ayuda a mantener mensajes consistentes cuando cambia el modelo de datos.


