Evitar fallos por datos nulos: restricciones, defaults y backfills
Evita fallos por datos nulos con constraints en la BD, defaults seguros, validación en la app y backfills para que prototipos con seed data se comporten bien en producción.

Por qué los datos seed ocultan problemas con nulos
“Funciona con seed data” suele significar que la app se probó con un conjunto pequeño y hecho a mano de registros limpios, completos y adaptados a la ruta feliz. Cada usuario tiene nombre, cada pedido tiene dirección y cada ajuste existe. El código parece correcto porque nunca afronta el desorden real.
El uso real crea huecos rápido. La gente abandona formularios, llegan hojas de cálculo con columnas vacías y APIs de terceros devuelven campos solo a veces. Incluso usuarios cuidadosos toman decisiones que dejan algo sin completar.
Los valores vacíos suelen aparecer tras el lanzamiento cuando:
- Un flujo de registro permite saltarse detalles del perfil y luego páginas posteriores asumen que existen
- Una importación CSV incluye emails en blanco, precios faltantes o fechas inconsistentes
- Un cliente móvil envía un payload parcial con una conexión inestable
- Un webhook cambia de forma, o un campo es null durante un incidente
- Un compañero edita datos manualmente y deja un campo requerido en blanco
Un fallo por datos nulos ocurre cuando tu código espera un valor y no recibe nada. La app intenta usar un campo vacío como si fuera dato real y lanza un error. Puede verse como “cannot read property of null”, una inserción en BD que falla, una página rota o un job en background que reintenta para siempre.
Evitar fallos por datos nulos no es solo tarea de código. Es tarea de reglas de datos. Necesitas dos cosas funcionando juntas:
-
Evitar que se guarden datos malos (para que la base de datos siga siendo fiable).
-
Manejar de forma segura los datos faltantes cuando están permitidos (para que la app siga funcionando y el usuario sepa cómo arreglarlo).
Los prototipos generados por IA son especialmente vulnerables aquí. La UI suele asumir entradas perfectas y la base de datos acepta cualquier cosa. Por eso un prototipo puede lucir bien en una demo y fallar en producción la primera vez que un usuario real se salta un campo o una importación trae huecos.
Una vez que tratas los seed data como el “mejor caso”, puedes diseñar para el caso normal: incompleto, inconsistente y a veces erróneo.
Empieza con reglas claras: ¿qué puede faltar?
La mayoría de las apps no fallan porque los datos sean “malos”. Fallan porque la app y la base de datos no coinciden sobre qué puede faltar. Antes de añadir constraints o escribir migraciones, redacta las reglas en inglés claro. Este paso suele prevenir más fallos por nulos que cualquier cambio de código aislado.
Empieza por decidir explícitamente “obligatorio vs opcional”:
- Email: obligatorio para login y reseteo de contraseña, o opcional si solo soportas teléfono o SSO
- Línea 2 de dirección: opcional
- Bio del perfil: opcional, pero la UI debe manejar estados vacíos
- Fecha de nacimiento: opcional, o requerida solo para funciones restringidas por edad
- Nombre de la empresa: opcional para individuos, obligatorio para cuentas de negocio
Luego separa tres estados que necesitan manejo distinto en la UI, la API y la base de datos:
- Desconocido: esperabas el valor pero aún no lo tienes (a menudo temporal)
- No proporcionado: el usuario eligió dejarlo en blanco
- No aplicable: el campo no tiene sentido para este registro
Ejemplo: en un flujo de registro, profile.bio es opcional. billing_country puede estar desconocido durante el registro, pero requerido antes de crear un cobro. vat_id puede no aplicar para muchos usuarios.
Después decide qué debe bloquearse vs qué puede permitirse pero manejarse. Bloquea cuando la app no puede funcionar o cumplir requisitos legales/seguridad sin el valor (identificadores de login, roles de permiso, IDs de propiedad). Permite-pero-maneja cuando puedes mostrar un fallback seguro (bio vacía, avatar faltante) o recopilarlo después.
Escribe tus reglas como una pequeña especificación antes de tocar código:
“User.email es obligatorio y único. User.address_line_2 es opcional. Profile.bio es opcional y se muestra vacío. Payment.country es obligatorio antes de crear un cargo.”
Si heredaste un prototipo generado por IA, esta especificación suele ser lo que falta. Las auditorías a menudo empiezan aquí porque revela dónde los nulos son aceptables y dónde deben bloquearse temprano.
Restricciones en la BD que impiden almacenar datos malos
Si quieres prevenir fallos por datos nulos, la base de datos tiene que decir “no” cuando la app intenta guardar algo que romperá más adelante. El código de la app cambia a menudo, pero las restricciones permanecen. Son una red de seguridad fiable, especialmente cuando un prototipo “funcionaba” solo porque se probó con seed data perfectos.
Usa NOT NULL para campos que realmente son requeridos
Añade NOT NULL solo a campos sin los que la app no puede funcionar. Una buena prueba es: “¿Puede un usuario real completar un flujo clave si esto falta?” Si no, hazlo obligatorio a nivel de BD.
Por ejemplo, una tabla orders normalmente no puede funcionar sin user_id y created_at. Si esos son nullable, acabarás con filas que parecen bien en la UI pero rompen reportes, emails o pantallas de administración.
Añade reglas simples con CHECK
Los CHECK son excelentes para reglas básicas y legibles que evitan valores basura:
- Rangos:
quantity > 0,price_cents >= 0 - Texto no vacío:
trim(display_name) <> '' - Valores permitidos:
status IN ('draft','active','canceled') - Fechas que tienen sentido:
end_date >= start_date
Estas reglas evitan datos “técnicamente no nulos, pero inútiles”. Una cadena vacía en un campo de nombre puede provocar la misma UI rota que un null.
La unicidad es otra fuente común de sorpresas. Usa UNIQUE para identificadores que no deben duplicarse, como email, username o un ID de proveedor externo (por ejemplo, google_sub). Sin ello, puedes acabar con dos cuentas que parecen el mismo usuario y el código downstream elegirá la fila “equivocada”.
Las claves foráneas también importan. Si permites filas hijas sin un padre (como registros profile sin user), el código que asume relaciones existentes fallará de formas extrañas. Las foreign keys previenen registros huérfanos y mantienen honestas las eliminaciones y actualizaciones.
Las constraints son la última línea de defensa, no la única. Tu app debería seguir validando la entrada y mostrando errores amigables, pero la base de datos debe hacer cumplir las reglas para que los datos malos no se filtren por jobs en background, scripts de admin o parches rápidos.
Defaults que hacen que los datos faltantes sean seguros (sin enmascarar problemas)
Los valores por defecto ayudan cuando un valor debe existir para cada fila, pero ni usuarios ni código deberían tener que proporcionarlo. Pueden prevenir fallos por nulos haciendo “vacío” imposible en lugares donde no tiene sentido.
Un buen default coincide con el comportamiento real del producto. Si no puedes explicar lo que significa a una persona de soporte, probablemente no sea un buen default.
Buenos defaults: aburridos, predecibles y correctos
Usa defaults para campos inevitables y que no son elección del usuario:
created_atoupdated_attimestamps- un
statusque empieza comopending,draftoactive - contadores como
login_countiniciados en0 - flags simples como
is_deletedenfalse
Estos defaults reducen la superficie de errores, especialmente en prototipos generados por IA donde algunas rutas de código olvidan establecer campos.
Defaults que ocultan bugs (y crean datos peores)
Los defaults se vuelven peligrosos cuando tapan relaciones faltantes o inputs requeridos. Hacen que la app parezca estable mientras rellenan la BD con tonterías.
Evita defaults como:
user_id = 0oaccount_id = 1cuando el propietario real es desconocidoemail = ''(cadena vacía) para saltarse un email faltanteprice = 0cuando se requería un precio para facturar correctamenterole = 'admin'porque la UI no envió un rol
Cuando luego añades constraints, los reportes, permisos y la lógica de facturación fallan de maneras confusas.
Si algo puede faltar por un tiempo, usa un estado explícito de “no listo aún”. Por ejemplo, un perfil puede empezar con status = 'incomplete' hasta que el usuario añada nombre y teléfono. Eso hace visible el estado faltante, testeable y fácil de manejar en la UI.
Un escenario rápido: un flujo de registro crea primero la fila de usuario y luego pide detalles del perfil en la siguiente pantalla. Un default seguro es profile_status = 'incomplete', no name = 'Unknown'. “Unknown” parece dato real, así que nadie lo corrige.
Validación en la app: atrapa problemas antes de llegar a la BD
Los fallos por nulos suelen comenzar antes de que la BD vea la petición. Un formulario envía un campo vacío, un cliente API olvida una propiedad o un webhook manda un payload ligeramente distinto al probado. La validación es tu primera línea de defensa. Convierte “error misterioso del servidor” en un mensaje claro y evita que entren datos malos.
Valida en el borde, lo más cerca posible de la entrada. Eso significa formularios en navegador, requests API e integraciones que postean datos a tu app (como proveedores de pagos o herramientas de email). Si aceptas input en tres lugares, necesitas cheques en los tres. El camino más débil es el que rompe producción.
Una buena validación no es solo “obligatorio o no”. También normaliza datos para que almacenes lo que esperas. Quita espacios extra, decide si los emails deben ser lowercased y maneja las cadenas vacías de forma consistente (a menudo como null, pero solo si tus reglas lo permiten). Normalizar temprano evita bugs sutiles como cuentas duplicadas porque un email tenía espacios al final.
Cuando la validación falla, devuelve mensajes humanos, no trazas de stack. Los usuarios deben saber qué arreglar y soporte debe poder reproducir el problema. Una respuesta 400 con una frase clara supera un error 500.
Un enfoque simple que funciona para la mayoría de apps:
- Valida en el servidor para cada escritura, aun si la UI ya valida
- Normaliza campos antes de guardar (trim, casing, manejo de cadenas vacías)
- Rechaza campos desconocidos para que errores tipográficos no se conviertan en nulos silenciosos
- Usa mensajes de error claros ligados al nombre del campo
- Loggea fallos de validación con suficiente contexto (pero nunca secretos)
Para evitar que las reglas diverjan, pon la validación en un solo lugar y reutilízala. Un modo común de fallo en la producción de prototipos generados por IA es un conjunto de reglas en la UI, otro distinto en la API y ninguna en el handler de webhook. Elige una fuente de verdad (a menudo un validador server-side o un esquema compartido) y que las otras capas la referencien.
Ejemplo concreto: tu formulario de registro requiere nombre completo, pero tu cliente móvil solo envía email y contraseña. Si el servidor no valida, podrías crear una fila de usuario con name = null y la primera página “Welcome, {name}” se rompe. Con validación server-side, la petición de registro falla rápido con “Nombre completo es requerido” y nunca guardas el registro roto.
Paso a paso: añadir constraints de forma segura en una app existente
Para prevenir fallos por nulos, trata las constraints como un rollout, no como un interruptor. El enfoque más seguro es aprender dónde existen nulos hoy, arreglar filas antiguas y luego bloquear nuevas filas malas.
Empieza inventariando qué puede ser null ahora mismo. Mira tus tablas y también dónde se usa cada campo: respuestas API, renderizado en UI, emails, jobs background y exports. Una columna que es inofensiva en una pantalla puede romper otra que asume que siempre está presente.
Un rollout práctico:
- Encuentra las áreas de riesgo. Lista columnas que permiten nulos y busca en tu código los lugares que asumen un valor (por ejemplo, llamar
.toLowerCase()en un nombre). - Decide la regla. Para cada columna elige una: debe estar presente, puede faltar, o puede faltar solo en ciertos momentos (por ejemplo “hasta completar onboarding”).
- Backfill de filas existentes. Actualiza registros antiguos para que coincidan con la regla. Si no puedes backfill correctamente, usa un estado temporal claramente marcado y un plan para recoger el valor real más tarde.
- Añade constraints en piezas pequeñas. Cambia una columna (o una tabla) por release. Mantén migraciones pequeñas para que los fallos sean fáciles de diagnosticar.
- Activa la constraint y obsérvala. Añade
NOT NULL,CHECK,FOREIGN KEYo reglas de unicidad solo después de que tus datos y el comportamiento de la app estén listos.
Después de desplegar, espera algunos fallos. Eso suele ser señal de que el sistema finalmente está atrapando problemas que antes ocultaba.
Añade monitorización simple alrededor de los nuevos puntos de fallo: cuenta errores de validación, monitorea errores de constraint en la BD y loggea el payload (sin datos sensibles) para ver qué falta y de dónde vino. Por ejemplo, si haces users.email no nulo, vigila aumentos provenientes de un camino de registro específico o de una build móvil antigua.
Ten un plan de rollback para migraciones que fallen en producción:
- Sabe cómo deshacer la constraint (o desactivar el check) rápidamente.
- Ten un script listo para re-ejecutar el backfill en lotes más pequeños.
- Asegura que tu app pueda manejar tanto el esquema viejo como el nuevo por un corto período.
Backfills: arregla filas existentes sin romper a los usuarios
Un backfill es una actualización planeada que llena valores faltantes para filas que ya están en la base de datos. Importa porque añadir un NOT NULL (o convertir una columna en requerida en la app) fallará si las filas antiguas todavía tienen nulos. Los seed data suelen verse perfectos. Los datos reales rara vez lo son.
Antes de tocar nada, decide cuál debe ser el valor correcto para campos faltantes. A veces un placeholder seguro está bien (como "Unknown" para una etiqueta de visualización), siempre que la app lo trate con claridad. Otras veces necesitas un valor derivado real. Por ejemplo, si users.timezone falta, podrías derivarlo del timezone más común en la organización del usuario, o de una región de registro si la guardas.
Los placeholders mantienen el sistema funcionando, pero pueden ocultar problemas si escoges valores que parecen reales. Un mal placeholder puede hacer que los dashboards mientan o que emails se envíen al nombre equivocado. En caso de duda, usa un valor que sea visiblemente “rellenado” u añade una flag separada como profile_incomplete = true para que sea fácil encontrar y corregir después.
Si la tabla es grande, haz el backfill por lotes. Lotes pequeños reducen el tiempo de bloqueo y la probabilidad de ralentizar la app en picos de uso:
- Actualiza un número limitado de filas por ejecución (por ejemplo 1.000 a 10.000)
- Ejecuta durante tráfico bajo y para si suben errores o la carga de la BD
- Mantén cada lote idempotente (seguro para reintentar)
Registra lo que cambias. Depurar es más fácil cuando puedes responder: “¿Qué filas se tocaron, cuándo y por qué versión del script?” Como mínimo, graba conteos e IDs. Si los datos son sensibles, registra solo IDs y un resumen.
Trata los backfills como dos cosas: un script puntual y un job repetible. El script puntual arregla los nulos de hoy. El job repetible atrapa datos que lleguen tarde (de workers antiguos, importaciones o reintentos fallidos) hasta que estés seguro de que todo respeta las nuevas reglas.
Ejemplo: un flujo de registro que falla por perfiles incompletos
Un fallo común de “funciona con seed data” se ve así: un usuario se registra, se crea la fila de cuenta, pero la fila de perfil solo se completa parcialmente. En datos de prueba, cada usuario tiene un perfil completo, así que nadie lo nota.
Aquí un escenario realista. Tu registro crea users y luego profiles. El perfil debería tener display_name y status, pero el código a veces salta display_name (por ejemplo, el usuario cierra la pestaña a mitad de onboarding). Más tarde, la app renderiza un header que asume que el nombre existe.
El fallo suele manifestarse como:
- Un error de servidor al renderizar una página (intenta formatear o pasar a mayúsculas un nombre null)
- Una respuesta API rota (un serializador espera una cadena y recibe null)
- Un error confuso en frontend (un componente lee
profile.display_name.lengthy falla)
Para prevenir fallos por nulos, la BD y la app deben coincidir en las mismas reglas, y las filas antiguas deben cumplir esas reglas también.
Plan de arreglo simple
Empieza por decidir qué significa “seguro” para un registro incompleto. Luego aplica la misma intención en cuatro sitios:
- Constraint en BD: exige una fila de profile para cada user y haz
statusNOT NULL. - Default: pon
statuspor defecto a algo comoincomplete. - Validación en la app: si
display_namees requerido para terminar onboarding, bloquea “Continuar” hasta que se provea, con un mensaje claro. - Backfill: actualiza usuarios existentes que tengan
statusnull (o perfiles faltantes) para que encajen con las nuevas reglas.
Tras el arreglo, la experiencia mejora. En vez de un crash en el dashboard, el usuario ve un aviso amable como “Completa tu perfil” y un formulario corto para añadir el nombre faltante. Tu API se mantiene predecible y el soporte recibe menos reportes de “ayer funcionaba”.
Errores comunes que hacen que los fallos por nulos vuelvan
La mayoría de equipos repite el mismo patrón: la app parece bien con seed data, pero un usuario real se salta un campo, una importación deja huecos o una integración envía payloads parciales. Si quieres evitar fallos por nulos a largo plazo, necesitas consistencia entre base de datos, app y los datos que ya tienes.
Un error común es añadir NOT NULL demasiado pronto. En una app en vivo, casi siempre hay filas existentes que no cumplen la nueva regla. El resultado: migración fallida o un hotfix apresurado que quita la constraint y te devuelve al punto de partida.
Otro problema sutil es usar cadenas vacías como sustituto de datos faltantes sin acordar su significado. Una cadena vacía puede significar “desconocido”, “no proporcionado” o “intencionalmente en blanco”. Si nadie lo define, los filtros y reportes quedan enmarañados y sigues teniendo crashes cuando el código asume que el valor es significativo.
Patrones que hacen que el problema reaparezca:
- Validación solo en el navegador mientras llamadas API, scripts y webhooks pueden enviar payloads malos
- Defaults que parecen útiles pero luego rompen facturación, impuestos o analítica
- Backfills que solo cubren el camino feliz y dejan filas raras hasta que un usuario las encuentra
- Tests que solo cubren inputs perfectos
- Seed data que permanece demasiado limpia y nunca incluye nulos, vacíos o relaciones faltantes
Los defaults “mágicos” merecen precaución extra. Si pones created_at a “ahora” en filas históricas faltantes, las gráficas de retención quedarán mal. Si defaulteas un precio faltante a 0, puedes regalar funciones de pago o sesgar métricas de ingresos. Los defaults deben asegurar la app, no inventar la verdad del negocio.
Una comprobación rápida: un flujo de registro puede permitir profile.bio opcional, pero luego una plantilla de welcome asume que la bio existe y crashea. Eso no es solo problema de BD; es un problema de contrato entre reglas, código y plantillas.
Si heredaste un código generado por IA (de herramientas como Lovable, Bolt, v0, Cursor o Replit), estos errores suelen aparecer juntos: validación server-side débil, manejo inconsistente de nulos y migraciones nunca probadas contra datos desordenados.
Lista rápida y siguientes pasos
Si quieres prevenir fallos por nulos, trata la falta de datos como normal, no como rara. Una buena solución suele ser enfocada: reglas claras, unas pocas constraints y un backfill cuidadoso.
Lista rápida
Empieza por anotar qué debe existir y qué puede quedar en blanco. Luego asegura que la misma regla se aplique en la base de datos y en la app.
- Lista los campos verdaderamente requeridos por cada tabla (y cada request API), y qué significa “falta” (NULL vs cadena vacía).
- Elige defaults solo donde tengan sentido (por ejemplo
status = 'draft') y evita defaults que oculten flujos rotos. - Confirma dónde ocurre la validación (form, API, job background) y asegúrate de que los errores sean claros para usuarios.
- Planea un backfill para filas existentes antes de activar nuevos
NOT NULL. - Elige un responsable para las reglas (producto decide qué se permite; ingeniería lo hace cumplir) y escribe las reglas en un solo sitio.
Haz una pasada rápida de “inputs malos” a continuación. Esto atrapa los caminos sorpresa que nunca existieron con seed data.
Tests rápidos para datos faltantes
Ejecuta estas comprobaciones en los viajes de usuario principales: registro, checkout, edición de perfil, importaciones y pantallas de admin, y cualquier endpoint público de API.
- Envía formularios con campos opcionales faltantes y con campos requeridos vacíos.
- Repite una importación con algunas columnas en blanco y espacios raros.
- Llama endpoints clave con claves JSON faltantes (no solo claves con null).
- Carga páginas de cuentas antiguas que pueden tener filas incompletas.
Para evitar que los problemas vuelvan, añade un pequeño set de tests de “nulos y vacíos” para tus endpoints y jobs más importantes. Incluso 5–10 casos pueden evitar que futuros cambios reintroduzcan fallos.
Si estás lidiando con una base de código generada por IA que se rompe cuando aparecen datos reales, FixMyMess (fixmymess.ai) puede empezar con una auditoría de código gratuita para encontrar las rutas riesgosas hacia nulos y luego aplicar reparaciones dirigidas como rollouts de constraints, arreglos de validación y backfills para dejar la app lista para producción.
Preguntas Frecuentes
¿Por qué mi app funciona con seed data pero falla en producción?
Los datos seed suelen estar elegidos a mano para ser completos y consistentes, por lo que tu código solo ve la “ruta feliz”. Usuarios reales, importaciones e integraciones introducen rápidamente campos faltantes, cadenas vacías y registros parciales que tu código no contempló.
¿Cuál es el primer paso para detener los fallos relacionados con nulos?
Empieza por escribir reglas en lenguaje claro para cada campo: obligatorio, opcional, o obligatorio solo en ciertas etapas (por ejemplo, “antes de cobrar una tarjeta”). El objetivo es que la app y la base de datos estén de acuerdo sobre qué puede faltar y cuándo.
¿Una cadena vacía es básicamente lo mismo que NULL?
NULL suele significar “sin valor en absoluto”, mientras que una cadena vacía sigue siendo un valor y a menudo se comporta distinto en consultas, validaciones y en la UI. Elige un significado por campo y normaliza la entrada para no mezclar ambos.
¿Cuándo debo añadir constraints NOT NULL?
Usa NOT NULL para campos sin los que el producto no puede funcionar, como IDs de propiedad, identificadores de login o timestamps requeridos. Si no estás seguro, trátalo como opcional primero y aplica NOT NULL solo cuando todas las rutas de escritura lo respeten.
¿Qué problemas previenen las restricciones CHECK?
Los CHECK son útiles para reglas simples que evitan valores inútiles incluso cuando no son nulos: cantidades negativas, rangos de fecha imposibles o texto “vacío pero no null”. Evitan datos que parecen válidos pero rompen facturación, informes o UI.
¿Qué tipos de defaults son seguros y cuáles son peligrosos?
Los defaults son buenos para valores que siempre deberían existir pero no deben depender del cliente, como timestamps, estados iniciales o contadores. Evita defaults que inventen la verdad del negocio (por ejemplo, precio faltante = 0 o asignar un owner falso), porque ocultan errores y contaminan los datos.
¿Dónde debe vivir la validación para evitar que entren nulos?
Valida en el servidor en cada escritura, aunque la UI ya valide, porque scripts, webhooks, importaciones y clientes antiguos pueden saltarse el navegador. Devuelve un error claro 400 con mensaje por campo para fallar rápido en lugar de almacenar filas rotas y fallar después.
¿Cómo añado constraints de forma segura en una app en producción?
Audita los nulos existentes, decide la regla por columna, rellena (backfill) las filas viejas y añade constraints en pequeñas entregas. Si activas restricciones antes de limpiar los datos antiguos, las migraciones pueden fallar o forzarte a revertir rápidamente.
¿Qué es un backfill y por qué lo necesito antes de añadir NOT NULL?
Un backfill actualiza filas existentes que violan tus nuevas reglas para que puedas aplicar constraints de forma fiable. Usa valores derivados correctos cuando sea posible; si debes usar marcadores, que sean obviamente “incompletos” para que no se confundan con datos reales.
¿Por qué los prototipos generados por IA son especialmente propensos a fallos por datos nulos, y qué puedo hacer al respecto?
Los prototipos generados por IA suelen asumir inputs perfectos y faltarles validación consistente en el servidor, constraints de esquema y defaults seguros, así que los datos desordenados reales los rompen rápido. Si heredaste una app generada por IA, FixMyMess (fixmymess.ai) puede empezar con una auditoría de código gratuita y luego aplicar arreglos dirigidos como rollout de constraints, correcciones de validación y backfills para dejar la app lista para producción en días, a menudo 48–72 horas.