Beyond Try-Catch: AI-Generated Mobile Code Error Handling
By Sanket Sahu
15th Sep 2026
Last updated: 15th Sep 2026
Ask an LLM to write a React Native screen and it will happily give you working code. Ask it to write a database migration with row-level security, a seed file that respects those policies, and a component that survives the seed being wrong, and the cracks show up fast. AI-generated code error handling is not about wrapping things in try-catch. It's about designing constraints the model cannot violate, feedback the model can act on, and recovery that survives the failures no one caught.
That is the architecture we built at RapidNative — an AI mobile app builder that turns natural language into production-ready React Native and Expo code. This post walks through how we actually handle errors in AI-generated mobile code across four layers: preflight validation, structured feedback to the model, runtime recovery in the preview, and cross-surface reconciliation between the editor and the physical device.
Photo by Ilya Pavlov on Unsplash
Why LLMs Fail at Error Handling (Even When You Ask Them To)
Recent industry write-ups have converged on the same observation: LLMs optimize for the happy path. When they do add error handling, they either wrap everything in a generic try-catch that swallows the actual failure, or they add validation for scenarios that cannot happen while missing the ones that will. Distributed data integrity — transactional rollbacks, cross-file consistency, RLS policies that match the auth model — is where they fall apart most often.
The naive fix is prompt engineering: "handle errors carefully, use transactions, check for null." This produces marginally better code and dramatically worse reasoning, because you have taught the model to add ceremony without teaching it what "correct" means for your runtime. A validator more permissive than the runtime cannot do its job.
The right fix is to move validation out of the model and into the tools the model calls. The model does not need to be trustworthy. The tools need to be honest.
Layer 1: Preflight Validation — Fail at Write Time, Not Run Time
Every write the agent performs — a new SQL migration, a seed file, a source-code edit — goes through a tool. That tool is not a passive file writer. It is a validator that runs the write against the real runtime before the file lands on disk.
The PGlite Story: Why We Threw Away the Fast Engine
Our agent generates full-stack apps with a Supabase-compatible backend. When it writes a migration with db_migration_new({ name, sql }), the tool needs to know whether that SQL will apply cleanly on top of everything that came before. Our first implementation used pg-mem, an in-memory Postgres subset. It was fast, synchronous, and mostly right.
Mostly right is a specific kind of dangerous.
We shipped a project whose first migration declared profiles.id as text and then defined RLS policies that compared it to auth.uid(). pg-mem happily accepted uuid = text. Real Postgres — and Supabase — does not: there is no such operator, and CREATE POLICY refuses. The whole migration chain aborted on the production side. No tables. No seed. Every screen empty. And our tool had reported success.
We swapped the validator for PGlite, which is real Postgres compiled to WebAssembly. Migrations now apply in-memory against actual Postgres semantics before the file is written. If PGlite rejects it, the tool returns { ok: false, error, hint } and the file never lands. If PGlite accepts it, the file writes and PGlite's post-migration state becomes the source of truth for every subsequent tool call — db_tables, db_describe, db_sql. Reads and writes are answered by the same engine that just validated them, so the model's mental model matches reality.
The rule we learned the hard way: your validator must be your runtime, not a subset of it. Swapping the engine back to buy speed would silently reintroduce the bug class.
Photo by Alexandre Debiève on Unsplash
The Lint Cascade: Turning Silent Bugs into Loud Errors
Applying cleanly is necessary but not sufficient. A migration can be syntactically valid Postgres and still produce a broken app. The most notorious example: RLS enabled with no policies. Postgres accepts this without complaint. It also returns zero rows to every authenticated query. The screen renders empty, no error appears anywhere, and the model has no way to know what went wrong.
Our lint layer catches seven structural issues after every successful migration:
- Missing PRIMARY KEY — error
- RLS enabled with no policies — error
- World-writable policies (e.g.
USING (true)on write operations) — warning - Foreign key without a supporting index — warning
updated_atcolumn with no trigger — warninguser_idcolumn with no policy that scopes on it — warning- Duplicate identifiers across seed and migration — error
Errors block the write and are returned to the model verbatim. Warnings pass through but attach to the tool result. The model gets a formatted string like:
ERROR: table "orders" has RLS enabled but no policies.
Add at least one policy (e.g. USING (auth.uid() = user_id))
or the table returns zero rows.
That single sentence — the rule, the fix, the consequence — is what the model needs to correct itself in one turn instead of five.
Layer 2: The Result Pattern — How Errors Flow Back to the Model
Every tool in the agent returns a discriminated union: { ok: true, ...data } or { ok: false, error, hint }. This is the Rust Result pattern, and it is the single most important design decision in the entire feedback loop.
Exceptions do not work here. If a tool throws, the error either propagates and kills the turn (bad — the model learns nothing) or gets swallowed by generic middleware (worse — the model thinks the operation succeeded). Neither of those creates a signal the LLM can reason about.
A structured result does. When db_migration_new rejects a duplicate primary key, the model sees:
{
"ok": false,
"error": "duplicate key value violates unique constraint \"orders_pkey\"",
"hint": "The seed at supabase/seed.sql already inserts orders with id 1-5.
Either change your migration's default seed values or update the seed."
}
The error field is the raw Postgres message so the model can pattern-match on failure modes it has seen in training. The hint field is the human-authored guidance that connects the failure to the actual project state. Together they turn a one-line database error into a fix the model can apply on the next tool call.
Streaming Feedback in Real Time
Our AI generation route streams tool events over Server-Sent Events: tool-input-start → tool-input-available → tool-output-available. The client sees the model's decisions unfolding, and the model's own subsequent turns see every tool output that has already resolved. This is what makes the recovery loop tight — the model does not wait until the end of the generation to find out a migration failed. It sees the rejection, reads the hint, and corrects mid-stream.
Photo by Emile Perron on Unsplash
Layer 3: Runtime Recovery — When Validated Code Still Breaks
Preflight validation catches the failure modes the tool layer knows about. Runtime is where the unknown ones show up: a Metro bundler error triggered by a package.json change, an unmatched route because a screen was renamed but the tab bar was not, a whitescreen because a component threw during render and there was no error boundary above it.
For those, we built a four-layer recovery cascade that runs inside the preview iframe.
Layer 1 — Auto-fix with AI. When the preview surfaces a build error or an unmatched route, the orchestrator pauses for two seconds (to let the error stream settle — build errors tend to cascade), then feeds the error text back into the agent as a fresh instruction. We cap this at two auto-retries so the loop cannot run away.
Layer 2 — Iframe reload. If auto-fix does not resolve the error, the iframe is reloaded. HMR is preserved where possible; only the failed module is re-required.
Layer 3 — Full page reload. The editor's Redux state is persisted to sessionStorage before the reload, so the user's chat history, selected artboard, and open files survive. On boot, state is rehydrated and the preview reconnects.
Layer 4 — Surface to the user. If three layers of automatic recovery fail, the error is elevated to a visible banner with the raw stack trace and a "Try to fix" button that re-runs Layer 1 on demand.
The cascade also has a suppression cooldown of 15 seconds after any user-initiated stop. Without it, hitting "stop generating" while the agent was mid-fix would trigger an infinite storm of reload attempts. We learned that one from a support ticket.
Layer 4: The Diagnostic That Makes the Invisible Visible
The hardest bugs in this pipeline are the ones with no visible error at all. A screen that renders empty because Metro rebuilt the bundle but the iframe never received the reload signal. A hot update that reports "0 modules changed" because the new file exists but nothing imports it yet. A whitescreen that lasted 400ms and cleared before anyone looked.
For those, we shipped a browser-side diagnostic called window.__rnLog. It runs on every project page in development and records:
- Metro stdout (
hot update (N module(s)),bundled in Xms) - HMR events and any bracketed sandbox log lines
- Every
rn-preview-reloadevent and the reload decisions (scheduled, firing, waiting, gave up) — these produce no console output, so without explicit instrumentation they were invisible - 500ms change-only samples of iframe URL, on-screen text,
data-bx-pathcontributors, and route-context size
The output is a JSON timeline you can download and inspect. That timeline is what actually located five separate bugs in the preview pipeline that we had been misdiagnosing for weeks — including one where the reload was firing before the file write had propagated to Metro's watcher, so the reload discarded the still-in-flight update. No stack trace would have surfaced that. Only a timeline showing "write at t+0ms, reload scheduled at t+30ms, hot update at t+180ms with 0 modules changed" made the ordering visible.
The lesson generalizes: when your errors are timing bugs across independent processes, logging is not observability. Sampling with millisecond timestamps is.
Cross-Surface Reconciliation: Editor vs. Device
A subtle failure mode in AI-generated mobile code is that a project has two previews, and they run on different infrastructure. The editor iframe renders on an in-browser sandbox that reads the project's virtual file system directly — the same files the agent writes. The physical device (Expo Go via QR code, or the shared web bundle) runs on a mobile workload synced to a cloud host, orchd.
They usually agree. When they diverge, the class of error is specific and worth naming.
A package.json change forces Metro to restart on the device workload. HMR cannot patch a dependency change; it needs a cold start. But Expo Go on the physical phone does not follow the restart — it silently keeps the bundle it already loaded. The result: the editor shows the new code, the phone shows the pre-generation template, and the user reports "blank screen on device."
The correct diagnosis is not "the app crashed." It is "the device is holding a stale bundle from before the restart, and the fix is a manual reload in Expo Go." We surface this now with a targeted hint whenever we detect a package.json change followed by no reconnect from the device workload within 30 seconds. Before we added that hint, it was our most common support ticket.
The general principle: when a system has two authoritative surfaces, your error handling has to cover the disagreement, not just the failures.
Photo by Yura Fresh on Unsplash
What This Looks Like in a Real Generation
To make the pieces concrete, here is what happens when a user asks RapidNative for "a fitness tracker app with workouts stored per user":
- The multi-LLM router picks a model and streams the response.
- The agent calls
db_migration_newwith aworkoutstable. PGlite applies it. Lints run. RLS is enabled and there is no policy. The tool returns{ ok: true, path, lints: "ERROR: ...", needsFix: true }and the model sees the lint immediately. - Next turn: the agent adds a
USING (auth.uid() = user_id)policy. PGlite applies. Lints pass. db_seedinserts three sample workouts. The tool applies the seed as the authenticated demo user — RLS blocks two of the three rows because theiruser_iddoes not match. The tool returns a warning: "2 of 3 rows are hidden by RLS from the demo user." The model reads it, updates the seed to useauth.uid(), and re-runs.- The agent writes the screen. It streams into the VFS. The editor iframe hot-updates. The route renders with data.
- The device workload syncs the same files. Expo Go picks up the HMR update. QR-code preview renders.
At every step, the failure modes have a place to go. The migration failure surfaces at the tool boundary. The seed visibility issue surfaces as a warning attached to the tool result. The screen render happens in a preview that would catch a build error and initiate the four-layer recovery cascade. And if the device does not reconnect, the cross-surface reconciliation surfaces a specific hint instead of a generic "something went wrong."
Three Takeaways for Anyone Building on LLM Code Generation
Move validation into your tools. The model does not need to be trustworthy. It needs feedback it can act on. A tool that returns a structured { ok, error, hint } is more useful than a model that has been prompted to "handle errors carefully."
Your validator must be your runtime. Anything less is a bug factory. If you validate SQL with a Postgres subset, production Postgres will reject things you accepted. If you validate TypeScript with a permissive parser, the strict compiler will reject things you shipped.
Instrument the invisible. The failures that hurt most are the ones with no error. Timeline logging, decision logging, and change-only sampling turn silent failures into diagnosable ones. Add them before you need them, because when you need them you cannot reconstruct what happened.
Try It Yourself
If you want to see this in action, start a new project on RapidNative and give it a prompt that will fail on purpose — ask for a users table with RLS but no policies, or a seed that violates a foreign key. Watch the agent catch the failure, read the hint, and correct itself. That is the four-layer architecture doing its job.
For a deeper look at the surrounding systems, our team writes about the full-stack template, validation before preview, and the export pipeline that ships this code to the App Store. The story of production-ready AI-generated code is the story of the constraints that catch what the model gets wrong.
Ready to build your app?
Turn your idea into a production-ready React Native app in minutes.
Free tools to get you started
Free AI PRD Generator
Generate a professional product requirements document in seconds. Describe your product idea and get a complete, structured PRD instantly.
Try it freeFree AI App Name Generator
Generate unique, brandable app name ideas with AI. Get creative name suggestions with taglines, brand colors, and monogram previews.
Try it freeFree AI App Icon Generator
Generate beautiful, professional app icons with AI. Describe your app and get multiple icon variations in different styles, ready for App Store and Google Play.
Try it freeFrequently asked questions
What is RapidNative?
RapidNative is an AI-powered mobile app builder. Describe the app you want in plain English and RapidNative generates real, production-ready React Native screens you can preview, edit, and publish to the App Store or Google Play.
Can I export the code?
Yes. RapidNative generates clean React Native and Expo code that you can export at any time. No lock-in, no proprietary format. Hand it to your developers or keep building inside RapidNative.
Is RapidNative free to use?
Yes. You can build apps on the free plan with no credit card required. Paid plans unlock unlimited AI generations, code export, and direct publishing to the App Store and Google Play.
Do I need to know how to code?
No. Most users build apps by describing what they want in plain English. Developers can drop into the code whenever they want more control, but coding is optional.
How long does it take to build an app?
Most users have a working first screen in under a minute. A full MVP usually takes a few hours instead of the weeks or months traditional development requires.