SYS://FIELD-REPORT-07
One Thrown Error, and the Whole App Goes White
The app worked. It looked finished. And it was one thrown render error away from going completely, blindingly white — not for a component, not for a page, but for the entire app, for every user at once. The owner had never seen it happen. That’s the whole problem: the person building it never does.
This is a representative audit — the exact pattern I hit over and over on apps built with Lovable, Bolt, v0, and Cursor. Names and paths are changed, but every finding is real-shaped, drawn straight from the kind of report the launchworthy skill produces. Call the app plotline: a Vite + React project built with Lovable, pre-launch, about to be handed to its first cohort of real users.
The scorecard led with Frontend & Experience. It was not a flattering read.
No criticals on this one — and I want to be honest about that up front, because the temptation is always to dress a finding up scarier than it is. The worst thing here is a [HIGH], not a [CRITICAL]. But a [HIGH] that can blank the whole app for everyone, on a code path you’ve never personally walked, is exactly the kind of finding that “works for me” is built to hide.
The finding that ends the app: no error boundaries
React has one unforgiving rule about uncaught render errors: an error thrown while rendering, with no boundary above it to catch it, unmounts the entire component tree. Not the broken card. The whole app. The user gets a white screen and a clean console they can’t see.
plotline had zero error boundaries. No ErrorBoundary components, no error.tsx, nothing wrapping the routes in src/App.tsx. So the app’s resilience to any render-time throw was exactly nothing.
And there was a throw waiting. src/components/ProjectCard.tsx:18 did this:
export function ProjectCard({ project }: { project: Project }) {
return (
<article>
<h3>{project.title}</h3>
{/* one row where owner is null → .name throws → whole app unmounts */}
<p>by {project.owner.name}</p>
</article>
);
}
On the owner’s machine every project.owner was populated, because the owner’s seed data was clean. In production one row had a null owner — a user who’d deleted their account, a half-finished import, a race on signup. null.name throws. With no boundary above it, React tears down the tree and paints white. One bad row takes down the app for everyone who loads that list.
CRITICAL · CAUGHT
No error boundaries anywhere — one throw white-screens the whole app
Not a broken card. The entire app unmounts for every user the moment any component throws on a shape you didn’t expect — a null field, an empty API response, a mistyped prop. And because nothing is tracked, the first report you get is a stranger saying “the site is just blank.”
The checklist grades this honestly: React with no ErrorBoundary / error.tsx covering major routes is [HIGH] — fix this week, not fix-before-launch. But it’s the [HIGH] I’d fix first, because its blast radius is everyone and its trigger is any data you didn’t anticipate.
The fix is small and boring, which is the best kind.
THE FIX · 20 MIN
Wrap your routes in an error boundary so a throw degrades to a fallback UI instead of a white screen. A minimal class boundary, or react-error-boundary’s <ErrorBoundary>, around <Routes> in src/App.tsx — plus per-route boundaries around the risky lists — turns “whole app gone” into “one card says something went wrong.” Then fix the actual throw: guard project.owner?.name ?? "Unknown" so the boundary never has to fire on that path in the first place.
The false alarm: “it works fine for me”
Here’s the part that decides whether you can trust an audit at all. Point plotline’s owner at the finding and the answer comes back fast and reasonable: “But it works fine for me. I click through the whole app every day and I’ve never seen a white screen.”
That’s not a lie. It’s just the least representative test in existence.
FALSE POSITIVE
"It works fine for me" — so there's no bug
You are one user, on fast wifi, with a warm cache, logged in as yourself, walking the happy path through data you seeded. Production is a hundred strangers on bad connections, cold caches, and half-broken rows, one of whom loads the exact list with the null owner. “Works for me” isn’t evidence the bug is absent. It’s evidence you’re the one person structurally guaranteed not to hit it.
The white screen is invisible on your own screen precisely because you’re the wrong person to find it. You never deleted your own account mid-session. Your API never returned an empty array during your demo. The render error lives in the gap between your data and the data a stranger will feed the app — and that gap is exactly where “looks finished” and “safe to launch” stop being the same thing.
The fix in order, because sequence matters here:
THE FIX · IN ORDER
Stop the white screen, then stop the throw
- Add a top-level boundary around the router in
src/App.tsxso any uncaught throw degrades to a fallback, not a blank page. This alone converts a total outage into a contained one. - Add per-route boundaries around the data-heavy views (the project list, the dashboard) so one broken card doesn’t blank its whole page either.
- Fix the specific throw at
ProjectCard.tsx:18with optional chaining and a fallback, so the boundary is your net, not your primary defense.
Verify it by hand This is the console-errors manual check, and it’s the one you can’t skip. Open the app, open DevTools → Console, and click through every main flow while watching for red. Then force the failure: feed the project list a row with a null owner (seed one, or null it in a fixture) and confirm you get the fallback UI, not a white page. Evidence: a screenshot of the fallback rendering where the app used to disappear.
The rest of the punch list
The error boundary is the headline. The rest of Frontend & Experience is the same theme — the app is silent when things aren’t perfect — ranked so the scope stays honest:
FIX THIS WEEK
After the boundary
- [HIGH] No loading states on the async routes.
src/pages/Projects.tsxfetches on mount and renders a blank screen until the data lands — on a slow connection that reads as “the app is broken,” not “the app is thinking.” Add a Suspense fallback or an explicit loading branch. - [HIGH] No global error handler. Nothing catches errors that escape the render tree entirely (async rejections, event handlers). Pair the boundary with a top-level handler so nothing throws into the void.
- [MEDIUM] No empty states. A new user with zero projects sees a void where
projects.map(...)renders nothing, not a “create your first project” prompt. Add an explicitlength === 0branch. - MANUAL CHECK Console errors on the main flows. Red in the console on the happy path is the smoke that leads to the next white screen — walk every page and paste anything red, or confirm it’s clean.
And here’s the bridge that makes all of this worse than it looks: Operations & Recovery. plotline has no error tracking — no Sentry, no global handler wired to report. So when the white screen fires in production, nothing records it. No stack trace, no count, no alert. The owner finds out when a user gives up and leaves, which means the metric for this bug isn’t an error dashboard — it’s churn. You don’t get to fix what you can’t see, and right now this app can’t see any of it.
ASK YOURSELF
“It works fine for me.” You are one user on fast wifi with a warm cache and no malice. Production is a hundred strangers on bad connections, one of whom is curious what happens when the data isn’t perfect. “Works for me” is the starting line, not the finish.
Why this keeps happening
None of this means Lovable did something wrong, or that plotline’s owner was careless. AI builders are extraordinary at getting you to looks finished — a clean render of the happy path, seeded with data that never fights back. They are structurally quiet about the unhappy path, because the unhappy path never shows up on the screen of the person building it. Error boundaries, loading states, empty states: these are the scaffolding for moments you personally never trigger, so they’re the first thing to get skipped and the last thing you’d notice missing.
That gap between looks finished and safe to launch is the entire reason launchworthy exists. It plays bouncer at the door of production: it knows a null field on one row can blank your whole app, it refuses to call a flow safe just because it works on your machine, and it hands you the fix, not just the finding — worst-first, so you know which one thing to do before you send the link.
plotline moved from a do-not-launch white-screen risk to a contained, observable app in an afternoon. The hard part was never wrapping a boundary. It was knowing that the app most likely to blank on a stranger is the one that has never once blanked on you.