Prevent null data crashes: constraints, defaults, and backfills
Prevent null data crashes with DB constraints, safe defaults, app validation, and backfills so seed-data prototypes behave reliably in production.

Why seed data hides null problems
“Works with seed data” usually means the app was tested with a small, hand-made set of records that are clean, complete, and shaped for the happy path. Every user has a name, every order has an address, and every setting exists. The code looks fine because it never meets real mess.
Real usage creates gaps fast. People abandon forms, spreadsheets arrive with blank columns, and third-party APIs return fields only sometimes. Even careful users make choices that leave something missing.
Empty values often show up after launch when:
- A signup flow allows skipping profile details, then later pages assume they exist
- A CSV import includes blank emails, missing prices, or inconsistent dates
- A mobile client submits a partial payload on a flaky connection
- A webhook changes shape, or a field is null during an outage
- A teammate edits data manually and leaves a required field blank
A null-data crash happens when your code expects a value but gets nothing. The app tries to use an empty field like it’s real data and throws an error. That can look like “cannot read property of null,” a failed database insert, a broken page, or a background job that retries forever.
Preventing null data crashes isn’t just a coding task. It’s a data rules task. You need two things working together:
-
Stop bad data from being stored (so the database stays trustworthy).
-
Handle missing data safely when it’s allowed (so the app still works, and the user gets guided to fix it).
AI-generated prototypes are especially vulnerable here. The UI often assumes perfect inputs, and the database often accepts anything. That’s why a prototype can demo well, then break in production the first time a real user skips a field or an import includes blanks.
Once you treat seed data as “best case,” you can design for the normal case: incomplete, inconsistent, and sometimes wrong.
Start with clear rules: what can be missing?
Most apps don’t crash because data is “bad.” They crash because the app and the database disagree about what is allowed to be missing. Before you add constraints or write migrations, write down the rules in plain English. This one step often prevents more null crashes than any single code change.
Start by making explicit calls on “required vs optional”:
- Email: required for login and password reset, or optional if you support phone or SSO only
- Address line 2: optional
- Profile bio: optional, but the UI must handle empty states
- Date of birth: optional, or required only for age-gated features
- Company name: optional for individuals, required for business accounts
Next, separate three states that need different handling in the UI, the API, and the database:
- Unknown: you expected the value, but you don’t have it yet (often temporary)
- Not provided: the user chose to leave it blank
- Not applicable: the field doesn’t make sense for this record
Example: in a signup flow, profile.bio is optional. billing_country might be unknown during signup, but required before creating a payment. vat_id can be not applicable for many users.
Then decide what should be blocked vs allowed-but-handled. Block when the app cannot function or meet legal/security needs without the value (login identifiers, permission roles, ownership IDs). Allow-but-handle when you can show a safe fallback (empty bio, missing avatar) or collect it later.
Write your rules as a short spec before touching code:
“User.email is required and unique. User.address_line_2 is optional. Profile.bio is optional and displays as empty. Payment.country is required before creating a charge.”
If you inherited an AI-generated prototype, this spec is usually what’s missing. Audits often start here because it reveals where nulls are acceptable and where they must be stopped early.
DB constraints that prevent bad data from getting stored
If you want to prevent null data crashes, the database has to say “no” when the app tries to save something that will break later. App code changes often, but constraints stay put. They’re a dependable safety net, especially when a prototype “worked” only because it was tested with perfect seed data.
Use NOT NULL for fields that are truly required
Add NOT NULL only to fields your app can’t function without. A good test is: “Can a real user complete a key flow if this is missing?” If not, make it required at the database level.
For example, an orders table usually can’t work without user_id and created_at. If those are nullable, you’ll eventually get rows that look fine in the UI but break reporting, emails, or admin screens.
Add simple rules with CHECK constraints
CHECK constraints are great for basic, readable rules that prevent garbage values:
- Ranges:
quantity > 0,price_cents >= 0 - Non-empty text:
trim(display_name) <> '' - Allowed values:
status IN ('draft','active','canceled') - Dates that make sense:
end_date >= start_date
These rules stop “technically non-null, still unusable” data. An empty string in a name field can cause the same broken UI as a null.
Uniqueness is another common source of surprises. Use UNIQUE constraints for identifiers that must never duplicate, like email, username, or an external provider ID (for example, google_sub). Without this, you can end up with two accounts that look like the same user, and downstream code will pick the “wrong” row.
Foreign keys matter too. If you allow child rows without a parent (like profile records without a user), code that assumes relationships exist will fail in strange ways. Foreign keys prevent orphan records and keep deletes and updates honest.
Constraints are the last line of defense, not the only one. Your app should still validate input and show friendly errors, but the database should enforce the rules so bad data can’t slip in through background jobs, admin scripts, or quick patches.
Defaults that make missing data safe (without masking issues)
Defaults help when a value should exist for every row, but users and code shouldn’t have to supply it. They can prevent null data crashes by making “empty” impossible in places where “empty” isn’t meaningful.
A good default matches real product behavior. If you can’t explain what the value means to a support person, it’s probably not a good default.
Good defaults: boring, predictable, and correct
Use defaults for fields that are inevitable and not user choice:
created_atorupdated_attimestamps- a
statusthat starts aspending,draft, oractive - counters like
login_countstarting at0 - simple flags like
is_deletedstarting atfalse
These defaults reduce surface area for bugs, especially in AI-generated prototypes where some code paths forget to set fields.
Defaults that hide bugs (and create worse data)
Defaults become dangerous when they paper over missing relationships or required input. They make the app look stable while filling your database with nonsense.
Avoid defaults like:
user_id = 0oraccount_id = 1when the real owner is unknownemail = ''(empty string) to bypass a missing emailprice = 0when a price was required to bill correctlyrole = 'admin'because the UI didn’t send a role
When you later add constraints, reports, permissions, and billing logic break in confusing ways.
If something can be missing for a while, use an explicit “not ready yet” state. For example, a profile can start with status = 'incomplete' until the user adds a name and phone number. That makes the missing state visible, testable, and easy to handle in the UI.
A quick scenario: a signup flow creates a user row first, then asks for profile details on the next screen. A safe default is profile_status = 'incomplete', not name = 'Unknown'. “Unknown” looks like real data, so nobody fixes it.
App validation: catch issues before they hit the database
Null crashes often start before the database ever sees a request. A form submits an empty field, an API client forgets a property, or a webhook sends a slightly different payload than you tested with. Validation is your first line of defense. It turns “mystery server error” into a clear message and stops bad data from entering your system.
Validate at the edge, as close to the input as possible. That means browser forms, API requests, and integrations that post data into your app (like payment providers or email tools). If you accept input in three places, you need checks in all three. The weakest path is the one that breaks production.
Good validation isn’t just “required or not.” It also normalizes data so you store what you expect. Trim extra spaces, decide if emails should be lowercased, and handle empty strings consistently (often as null, but only if your rules allow it). Normalizing early prevents subtle bugs like duplicate accounts because one email had trailing whitespace.
When validation fails, return human messages, not stack traces. Users should know what to fix, and support should be able to reproduce the issue. A 400-level response with one clear sentence beats a 500 error.
A simple approach that works for most apps:
- Validate on the server for every write, even if the UI already validates
- Normalize fields before saving (trim, casing, empty string handling)
- Reject unknown fields so typos don’t silently become nulls
- Use clear error messages tied to a field name
- Log validation failures with enough context (but never secrets)
To keep rules from drifting, put validation in one place and reuse it. A common failure mode in productionizing AI-generated prototypes is one set of rules in the UI, a different set in the API, and no rules in the webhook handler. Pick a single source of truth (often a server-side validator or shared schema) and have other layers reference it.
Concrete example: your signup form requires a full name, but your mobile client only sends email and password. If the server doesn’t validate, you might create a user row with name = null, and the first “Welcome, {name}” page crashes. With server-side validation, the signup request fails fast with “Full name is required,” and you never store the broken record.
Step-by-step: add constraints safely in an existing app
To prevent null data crashes, treat constraints like a rollout, not a switch flip. The safest approach is to learn where nulls exist today, fix old rows, then block new bad rows.
Start by inventorying what can be null right now. Look at your tables, and also where each field is used: API responses, UI rendering, emails, background jobs, and exports. A column that is harmless in one screen can crash another that assumes it’s always present.
A practical rollout:
- Find the risk areas. List columns that allow nulls and search your code for places that assume a value (for example, calling
.toLowerCase()on a name). - Decide the rule. For each column, pick one: must be present, can be missing, or can be missing only at certain times (like “until onboarding is complete”).
- Backfill existing rows. Update old records so they match the rule. If you can’t backfill correctly, use a temporary, clearly marked state plus a plan to collect the real value later.
- Add constraints in small pieces. Change one column (or one table) per release. Keep migrations small so failures are easy to diagnose.
- Turn on the constraint, then watch it. Add
NOT NULL,CHECK,FOREIGN KEY, or unique rules only after your data and app behavior are ready.
After you ship, expect a few failures. That’s often a sign the system is finally catching problems it used to hide.
Add simple monitoring around the new failure points: count validation errors, track database constraint errors, and log the payload (without sensitive data) so you can see what’s missing and where it came from. For example, if you make users.email not null, watch for spikes from a specific signup path or an older mobile build.
Have a rollback plan for migrations that fail in production:
- Know how to undo the constraint (or disable the check) quickly.
- Keep a script ready to re-run the backfill in smaller batches.
- Make sure your app can handle both old and new schema for a short time.
Backfills: fix existing rows without breaking users
A backfill is a planned update that fills in missing values for rows already in your database. It matters because adding a NOT NULL constraint (or making a column required in your app) will fail if older rows still have nulls. Seed data often looks perfect. Real data rarely is.
Before you touch anything, decide what the correct value should be for missing fields. Sometimes a safe placeholder is fine (like "Unknown" for a display label), as long as the app treats it clearly. Other times you need a real derived value. For example, if users.timezone is missing, you might derive it from the most common timezone in that user’s organization, or from a recorded signup region if you store it.
Placeholders keep things moving, but they can hide problems if you pick values that look real. A bad placeholder can make dashboards lie or trigger emails to the wrong name. When in doubt, use a value that is obviously “filled,” or add a separate flag like profile_incomplete = true so it’s easy to find and fix later.
If the table is big, backfill in batches. Small batches reduce lock time and lower the chance you slow down the app during peak use:
- Update a limited number of rows per run (for example 1,000 to 10,000)
- Run during low traffic, and stop if error rates or DB load rises
- Keep each batch idempotent (safe to retry)
Log what changed. Debugging is easier when you can answer: “Which rows were touched, when, and by which script version?” At minimum, record counts and IDs. If the data is sensitive, log only IDs and a summary.
Treat backfills as two things: a one-time script and a repeatable job. The one-time script fixes today’s nulls. The repeatable job catches late-arriving data (from older workers, imports, or flaky retries) until you’re confident everything enforces the new rules.
Example: a signup flow that fails with incomplete profiles
A common “works with seed data” bug looks like this: a user signs up, an account row is created, but the profile row is only partly filled in. In test data, every user has a complete profile, so nobody notices.
Here’s a realistic scenario. Your signup creates users and then profiles. The profile should have display_name and status, but the code sometimes skips display_name (for example, the user closes the tab mid-onboarding). Later, the app renders a dashboard header that assumes the name exists.
The crash often shows up as:
- A server error when rendering a page (trying to format or uppercase a null name)
- A broken API response (a serializer expects a string, receives null)
- A confusing front-end error (a component reads
profile.display_name.lengthand fails)
To prevent null data crashes, the database and the app must agree on the same rules, and old records need to match those rules too.
A simple fix plan
Start by deciding what “safe” means for an incomplete signup. Then apply the same intent in four places:
- DB constraint: require a profile row for every user, and make
statusNOT NULL. - Default: set
statusdefault to something likeincomplete. - App validation: if
display_nameis required to finish onboarding, block “Continue” until it’s provided, with a clear message. - Backfill: update existing users who have null
status(or missing profile rows) so they fit the new rules.
After the fix, the experience improves. Instead of a crash on the dashboard, the user sees a friendly prompt like “Finish setting up your profile” and a short form to add the missing name. Your API stays predictable, and support gets fewer “it worked yesterday” reports.
Common mistakes that keep null crashes coming back
Most teams hit the same pattern: the app looks fine with seed data, then a real user skips a field, an import leaves blanks, or an integration sends partial payloads. If you want to prevent null data crashes for good, you need consistency across the database, the app, and the data you already have.
One common misstep is adding a NOT NULL constraint too early. In a live app, you almost always have existing rows that don’t match the new rule. The result is a failed migration, or a rushed hotfix that removes the constraint and quietly puts you back where you started.
Another subtle issue is using empty strings as a stand-in for missing data without agreeing on what that means. An empty string can mean “unknown,” “not provided,” or “intentionally blank.” If nobody defines it, filtering and reporting becomes messy, and you still get crashes when code assumes the value is meaningful.
Patterns that make problems recur:
- Validation only in the browser while API calls, scripts, and webhooks can still send bad payloads
- Defaults that look helpful but later break billing, tax, or analytics
- Backfills that only handle the happy path, leaving rare rows untouched until a user hits them
- Tests that only cover perfect inputs
- Seed data that stays too clean and never includes nulls, blanks, or missing relationships
Magic defaults deserve extra caution. If you set created_at to “now” for missing historical rows, retention charts will be wrong. If you default a missing price to 0, you may accidentally give away paid features or skew revenue metrics. Defaults should make the app safe, but they should not invent business truth.
A quick reality check: a signup flow might allow profile.bio to be optional, but later a welcome email template assumes bio is present and crashes. That’s not a database problem alone. It’s a contract problem between your rules, your code, and your templates.
If you inherited an AI-generated prototype (from tools like Lovable, Bolt, v0, Cursor, or Replit), these mistakes often show up together: weak server-side validation, inconsistent null handling, and migrations that were never tested against messy data.
Quick checklist and next steps
If you want to prevent null data crashes, treat missing data as normal, not rare. A good fix is usually focused: clear rules, a few constraints, and one careful backfill.
Quick checklist
Start by writing down what must exist and what can be blank. Then make sure the same rule is enforced in the database and in the app.
- List the truly required fields for each table (and each API request), and what “missing” means (NULL vs empty string).
- Choose defaults only where they make sense (for example, default
status = 'draft'), and avoid defaults that hide broken flows. - Confirm where validation happens (form, API, background job) and make sure errors are clear to users.
- Plan a backfill for existing rows before you turn on new
NOT NULLconstraints. - Pick an owner for the rules (product decides what’s allowed; engineering enforces it) and write the rules down in one place.
Do a fast “bad input” pass next. This catches the surprise paths that never existed with seed data.
Smoke tests for missing data
Run these checks on main user journeys: signup, checkout, profile edits, imports, admin screens, and any public API endpoints.
- Submit forms with optional fields missing, and with required fields empty.
- Replay an import with a few blank columns and weird whitespace.
- Call key endpoints with missing JSON keys (not just keys set to null).
- Load pages for older accounts that might have incomplete rows.
To keep issues from coming back, add a small set of “null and empty” tests for your top endpoints and background jobs. Even 5-10 cases can stop future changes from reintroducing crashes.
If you’re dealing with an AI-generated codebase that falls apart once real data shows up, FixMyMess (fixmymess.ai) can start with a free code audit to find the risky null paths, then apply targeted repairs like constraint rollouts, validation fixes, and backfills to make the app production-ready.
FAQ
Why does my app work with seed data but crash in production?
Seed data is usually hand-picked to be complete and consistent, so your code only sees the “happy path.” Real users, imports, and integrations quickly introduce missing fields, blank strings, and partial records that your code wasn’t written to handle.
What’s the first step to stop null-related crashes?
Start by writing plain-English rules for each field: required, optional, or required only at certain stages (like “before charging a card”). The goal is to make the app and database agree on what can be missing and when.
Is an empty string basically the same as NULL?
NULL usually means “no value at all,” while an empty string is still a value that often behaves differently in queries, validations, and UI rendering. Pick one meaning for “missing” per field and normalize input so you don’t end up with both forms mixed together.
When should I add NOT NULL constraints?
Use NOT NULL for fields the product truly can’t function without, like ownership IDs, login identifiers, or required timestamps. If you’re not sure, treat it as optional first, add app handling, and only enforce NOT NULL once you’re confident it’s required across every write path.
What kinds of problems do CHECK constraints prevent?
Use CHECK constraints for simple rules that prevent unusable values even when they’re not null, like negative quantities, impossible date ranges, or “blank but not null” text. They’re especially useful to stop “looks filled” data that later breaks billing, reporting, or UI assumptions.
Which database defaults are safe, and which ones are dangerous?
Defaults are good for values that should always exist but shouldn’t rely on the client to send them, like timestamps, initial statuses, or counters. Avoid defaults that invent business truth (like setting a missing price to 0 or assigning a fake owner), because they hide bugs and pollute your data.
Where should validation live to prevent nulls from slipping in?
Validate on the server for every write, even if your UI validates, because scripts, webhooks, imports, and older clients can bypass the browser. Return a clear 400-level error with a field-specific message so you fail fast instead of storing broken rows and crashing later.
How do I add constraints safely in a live app without breaking it?
Audit existing nulls first, decide the rule per column, backfill old rows to match the rule, then add constraints in small releases. If you turn on constraints before cleaning old data, migrations can fail or force rushed rollbacks that put you back where you started.
What is a backfill, and why do I need one before adding NOT NULL?
A backfill updates existing rows that already violate your new rules so you can enforce constraints reliably. Use correct derived values when possible, and if you must use placeholders, make them obviously “incomplete” so they don’t get mistaken for real user data later.
Why are AI-generated prototypes especially prone to null-data crashes, and what can I do about it?
AI-generated prototypes often assume perfect inputs and miss consistent server-side validation, schema constraints, and safe defaults, so messy real data triggers crashes quickly. If you inherited a broken AI-built app, FixMyMess can start with a free code audit and then apply targeted fixes like constraint rollouts, validation repairs, and backfills to make it production-ready in days, often 48–72 hours.