In this article
A polished React screen says almost nothing about production readiness. I would not ship AI-generated React code until its failure pipeline had received the same attention as its happy path.
That pipeline has three jobs: contain a failure at an honest boundary, send enough browser context for diagnosis, and move the interface into a valid recovery state. A top-level try/catch cannot do all three. Forwarding console output to an error service cannot either.
As I’ve argued before, an AI-ready application is defined by resilience, not by whether it includes a generative feature. Generated components can compile and look convincing while still mishandling asynchronous work, state transitions, accessibility, or uncommon browser conditions. The practical response is not to reject generated code. It is to review it as code that will eventually encounter something its author did not model.
Map Error Surfaces Before Choosing Tools
A global error handler can look like coverage while leaving the product without a coherent failure model.
Start by mapping where failures can occur:
- Component rendering
- Event handlers
- Asynchronous operations
- Data fetching and response parsing
- Browser APIs
- Route transitions
- Authentication and authorization flows
- Payments and data submission
- Persistence and state restoration
These surfaces fail differently.
React’s documentation is precise about boundary scope: an error boundary catches errors thrown while its descendant tree is rendering and during relevant React lifecycle work. It does not catch arbitrary event-handler errors, asynchronous callbacks such as setTimeout, server-side rendering failures, or errors thrown by the boundary itself. React’s official Component reference documents that distinction. Framework-level routing and rendering facilities may add their own boundary behavior, so check the framework’s rules rather than assuming every failure follows React’s component-boundary path. Browser-level handlers for global JavaScript errors and unhandled promise rejections remain separate, complementary layers.
A render exception may call for a local fallback and a component stack. A rejected request needs network context and a retry state. A browser API may fail outside React altogether. Calling all three a “frontend error” produces vague events and recovery controls that may not be safe.
Technical origin is only half the map. An avatar that fails to load is not equivalent to a checkout submission entering an uncertain state. Authentication, payments, data submission, and the product’s primary workflow warrant tighter boundaries, richer telemetry, and stricter tests.
The goal is deliberate coverage, not maximum instrumentation. Decide which failures can remain isolated, which require navigation to a stable route, and which make the current session too untrustworthy to continue.
Place Boundaries Around Recoverable Units
Recovery scope should determine boundary placement.
Route-level boundaries are a practical baseline because routes usually represent meaningful units of work. If one route crashes, the shell, navigation, and unaffected routes should remain available. A route failure should not turn the whole page into a blank screen.
One root boundary is often too coarse. Narrower boundaries make sense around risky or independently reloadable areas such as:
- A data-heavy dashboard panel
- A file preview
- An editor sidebar
- A third-party integration
- A complex visualization
- A lazy-loaded feature
Wrapping every component is not resilience. Boundaries that are too narrow create noise, fragment the fallback experience, and can hide a systemic defect behind a patchwork of error cards. The useful unit is the smallest area that can recover independently without making the surrounding state misleading.
Fallback UI should make three things clear: what is unavailable, whether the user’s work was preserved, and which action is safe now. Do not expose a stack trace, raw exception, internal path, or request body. A fallback is product UI, not an embedded debugging console.
Its controls need to be honest. “Try again” belongs only where repetition is safe. “Reset this view” may fit corrupted presentation state. “Go to dashboard” gives users a stable exit from a failed route. A full reload is a last resort when the application cannot otherwise reestablish trustworthy state.
A compact boundary can keep the recovery contract explicit:
type RecoveryState =
| { status: "ready" }
| { status: "failed"; retryCount: number; canRetry: boolean }
| { status: "retrying"; retryCount: number }
| { status: "exhausted" };
class RouteBoundary extends React.Component<
{ children: React.ReactNode },
{ recovery: RecoveryState }
> {
state = { recovery: { status: "ready" } as RecoveryState };
static getDerivedStateFromError(): { recovery: RecoveryState } {
return {
recovery: { status: "failed", retryCount: 0, canRetry: true }
};
}
render() {
if (this.state.recovery.status === "failed") {
return <RouteFallback canRetry={this.state.recovery.canRetry} />;
}
return this.props.children;
}
}
The example is intentionally small. Production code still needs event reporting, retry accounting, state reset behavior, and a stable escape route. The point is to represent recovery as a state contract instead of reducing it to a generic error flag.
When a boundary catches an error, useful diagnostic context can include the component stack, route, release identifier, pseudonymous session correlation ID, retry count, and an approved subset of relevant state. That connects recovery with diagnosis rather than merely suppressing the crash.
Build a Browser Telemetry Pipeline, Not a Bigger Console
Production browser failures occur across devices, networks, routes, releases, and sessions that a local console cannot reproduce. More console messages do not close that gap.
Useful events are structured and machine-readable. A stable schema might look like this:
{
"level": "error",
"event": "route_render_failed",
"timestamp": "ISO-8601 timestamp",
"route": "/account",
"release": "release identifier",
"environment": "production",
"sessionId": "pseudonymous correlation identifier",
"component": "AccountRoute",
"retryCount": 0,
"recoverable": true
}
The schema can vary, but free-form strings should not be its primary interface. Stable fields let operators group failures, correlate related signals, and separate a recurring defect from unrelated exceptions with similar messages.
A correlation ID is not the same as an authenticated user identifier. Prefer a short-lived, pseudonymous session or trace value when diagnosis only requires grouping related events. If an authenticated identifier is genuinely necessary, treat it as a separate, explicitly governed field rather than quietly overloading sessionId.
Release and environment fields should come from the build and deployment system—such as injected build metadata, an artifact version, or runtime configuration—not from user input or ad hoc component state. That makes an event traceable to the code and environment that produced it.
Capture failures reported by React error boundaries, global JavaScript errors, and unhandled promise rejections. Correlate those signals with network activity and session context only when the added data materially improves diagnosis. Stack traces, request failures, and route context can be useful together, but each extra field increases payload size and privacy risk.
Context needs a hard limit. Attaching an entire application store because one property might help is a poor tradeoff. So is serializing component props indiscriminately. Define a small diagnostic contract for each critical workflow: approved identifiers, state labels, attempt counts, route information, and sanitized response metadata.
Keep telemetry away from the immediate interaction path where practical, and use sampling when appropriate. Asynchronous delivery reduces direct blocking, but it does not make instrumentation free. Event construction, serialization, stack processing, and SDK hooks can still consume main-thread time. If diagnostics flood the network or amplify a failure loop, the observability system has become another production defect.
Sanitize Before Data Leaves the Browser
Browser error monitoring is a data pipeline, so sanitization belongs at the first stage.
An allowlist is the practical approach. Define which fields may leave the browser rather than collecting arbitrary state and trying to redact every dangerous field later. Retrospective redaction is brittle, especially when generated code introduces new props, URL parameters, or request shapes.
Keep these out of telemetry:
- Credentials and authentication tokens
- Personal data
- Form contents
- Sensitive URL parameters
- Full request and response bodies
- Complete state-store snapshots
- Unfiltered component props
The Datadog guide to React error monitoring treats collection as a pipeline that needs both diagnostic context and controls for sensitive data. That is the useful model here. Sanitization is part of event construction, not a downstream cleanup job.
Test what the browser actually emits. Trigger an error after a user completes a form, another on an authenticated route, and another after a malformed response. Inspect the resulting payload rather than stopping at the logging call. Credentials, form values, tokens, and sensitive parameters should be absent.
Generated instrumentation deserves extra scrutiny. Code generation tends to make logging broad because broad logging looks helpful in isolation. Reject any implementation that dumps stores, props, or request bodies without an explicit field contract.
Treat Recovery as UI State
Recovery is not a button attached to an exception. It is application state.
A workflow may need to represent idle, loading, success, empty, error, retrying, and exhausted-retry states. When those states exist only as overlapping booleans, contradictions are easy to create. A view can appear both loading and failed. A stale success screen can survive a rejected mutation. A retry button can remain active after an operation enters an uncertain state.
Model meaningful transitions directly. A transient network failure might move from loading to error, then to retrying. A persistent application defect should eventually reach an exhausted state instead of offering unlimited retries.
A retry budget keeps a broken endpoint or deterministic client defect from creating an endless loop. Once that budget is gone, send the user to a stable route or offer a non-retry escalation path.
Recovery operations also need to be idempotent. Repeated clicks cannot create duplicate submissions, duplicate payments, or conflicting mutations. Disable or deduplicate actions while an attempt is in flight, and distinguish a confirmed rejection from an operation whose outcome is unknown.
Preserve progress when doing so is safe. A failed preview should not erase valid editor content, and a broken sidebar should not reset an unrelated form. Corrupted, unverifiable, or malformed-response-derived state is different. Discard it rather than restoring it optimistically.
Complex editors need deliberate architecture for undo, redo, persistence, collaboration, and crash recovery. Generated state code is a proposal, not an architectural decision.
Harden Data Fetching and State Boundaries
Fetching data directly inside useEffect is not inherently wrong. It is often under-specified.
For each effect-based request, inspect cancellation on unmount or parameter change, races between responses, retry and cache behavior, deduplication, stale-data handling, offline transitions, and error classification.
When an application needs caching, invalidation, controlled retries, or request deduplication, a dedicated server-state layer such as TanStack Query or RTK Query is usually more reliable than rebuilding those policies across effects. The production-hardening workflow in Hardening an AI-Generated React App for Production makes the same separation.
There is a real complexity cost. A server-state library is unnecessary for a trivial, one-time request with no caching or synchronization requirements. It earns its place when request lifecycle behavior becomes part of the product.
Keep three categories conceptually separate:
- Server state: remotely owned data with fetching, freshness, and invalidation concerns
- Durable client state: local information that must survive navigation, reload, or recovery
- Temporary view state: open panels, selected tabs, draft filters, and other ephemeral controls
Collapse them into one global store and recovery becomes blunt. Resetting a failed view can erase durable work. Restoring the store can revive stale server data. A generated application may use whichever pattern appeared in its prompt; a human reviewer still has to decide whether those boundaries fit the product.
One supporting source, a React Native article on AI-generated state management, offers a relevant warning about generated architecture. React Native is a different runtime and platform, though, so it should not be treated as authority for browser-specific React behavior.
Run a Compact Six-Dimension Audit
A visual review does not establish production readiness. Use this pass before shipping:
| Area | Operator check | Refuse to ship when |
|---|---|---|
| Data | Verify cancellation, caching, invalidation, stale-state handling, offline behavior, and response ordering. | Dependency changes or unmounts can leave request behavior undefined. |
| Errors | Trace render, browser-level, promise, and request failures to the correct capture and fallback path. | An exception is swallowed, retried forever, or shown without a valid next step. |
| Types | Tighten TypeScript settings and validate uncertain external data at runtime. | Parsed responses, optional properties, or indexed values are trusted only because fixtures happened to contain them. |
| Performance | Look for duplicate requests, broad rerenders, expensive recomputation, and unjustified memoization. | Instrumentation or lifecycle defects create visible work on the main thread. |
| Accessibility | Test landmarks, keyboard access, focus movement, and announcements in fallback states. | A user cannot reach or understand the recovery controls without a pointer. |
| Observability | Inspect the emitted event for stable names, approved context, release metadata, and pseudonymous correlation. | The payload is unactionable or exposes sensitive data. |
Test Failure Paths as Product Flows
Success-path tests do not prove resilience. Force the application into render exceptions, rejected promises, request timeouts, malformed responses, offline transitions, route-loading failures, retry exhaustion, and corrupted persisted state.
Confirm that the smallest appropriate fallback appears and unaffected areas remain usable. A widget failure should not remove the whole route unless the route can no longer present truthful state.
Exercise recovery more than once. Click retry repeatedly. Navigate away while a request is active. Return after state has been persisted. Reload while the workflow is in an error state. Those actions must not duplicate submissions or restore corrupted data.
Test accessibility during failures, not only during normal rendering. Focus should move predictably, assistive technology should receive the error message and recovery controls, and keyboard users should be able to complete the path. The W3C’s guidance on managing focus and live regions provides primary accessibility context for those behaviors.
Finally, inspect the emitted event. Its stack and correlation fields should be useful, its event name stable, and its payload free of sensitive values. A working fallback paired with unusable telemetry is only half an implementation.
A Lean Definition of Done
For an important AI-assisted React change, I would expect intentional boundaries around critical routes and independently recoverable units, plus browser-level capture for failures outside boundary scope. Telemetry should be structured, sanitized, and cheap enough not to become its own failure mode. Recovery states should be explicit, retries limited and idempotent, valid progress preserved, and corrupted state removed.
Request cancellation, caching, stale-data, and retry policies should be defined rather than implied. Generated state architecture still needs human review. Failure paths belong beside success paths in the test plan, and every fallback should be accessible and offer only actions the application can safely perform.
AI-ready React apps do not need elaborate abstractions around every component. They need a failure pipeline whose boundaries, telemetry, and recovery semantics agree with one another. Tools can generate implementation candidates. Operators remain responsible for data policy, boundary placement, valid state transitions, and the decision to ship.
Sources and further reading
- React Component reference: Catching rendering errors with an error boundary
- Building AI-Ready React Apps: Resilience Through Error Boundaries and Logging
- Hardening an AI-Generated React App for Production
- Best Practices: React Logging and Error Handling
- A Practical Guide to React Error Monitoring
- State Management in AI-Generated React Native Apps
No comments yet