Experience Bytes: How We Built Self-Healing Next.js Apps with Fetch Interception and Error Boundaries
Project : Lyzr Architect
Learn something new everyday as a developer. Experience bytes is a collection of small blogs where I write about the challenges that I face as a full stack developer while building features for products. Which spans across all the way from distributed micro-services, security to mobile apps.
The context
While building Lyzr Architect, our AI agent generates Next.js apps that run in isolated iframes. Users interact with these generated UIs — chat interfaces, dashboards, forms. The problem? When those apps break — whether it’s an API response mismatch or a UI rendering bug — users see cryptic errors like “Cannot read property ‘message’ of undefined” and have no way to fix them. The AI that built the app can’t see the error. Dead end.
Why is runtime error handling a problem with AI-generated apps?
If you’ve built systems where LLMs generate frontend code, you’ve probably seen this: the AI creates a working Next.js app, but when the API response format changes slightly or a component hits an edge case, the app crashes. Runtime UI bugs, hydration errors, undefined property access — the usual Next.js pain points. The AI has no feedback loop to know something broke, let alone what broke.
Our generated apps were dying silently. Users were stuck staring at error screens with no recovery path.
How was it traditionally solved?
The naive approach is wrapping everything in try-catch and showing generic error messages. “Something went wrong. Please try again.”
Problems with this:
- User has no context on what actually failed
- The AI agent that built the app never learns about the error
- No path to recovery — user refreshes and hopes for the best
- Same bug keeps happening because nothing gets fixed
How does fetch interception solve this?
Instead of passive error handling, I built a system that:
- Monkey-patches the global fetch — intercepts all API calls without touching the generated app code
- Clones responses before inspection — the UI still gets its data, we’re just watching
- Detects specific failure modes — API errors, parse failures, network issues
- Bridges back to the parent AI — sends structured error context via postMessage so the AI can auto-repair
javascript
const originalFetch = window.fetch.bind(window)window.fetch = async (input, init) => {
if (!url.includes('/api/agent')) {
return originalFetch(input, init)
}
const response = await originalFetch(input, init)
const cloned = response.clone() // Clone so UI still gets data
const data = await cloned.json()
const issue = detectResponseIssue(data)
if (issue) {
showErrorModal(issue) // Let user click "Fix with AI"
}
return response // Always return original
}The key insight: we detect three distinct failure modes that each need different fixes:
Get Shreyas Kapale’s stories in your inbox
Join Medium for free to get updates from this writer.
javascript
function detectResponseIssue(data) {
// 1. API returned explicit error
if (data.success === false && data.error) {
return { type: 'api_error', message: data.error }
}
// 2. Parse failed but raw data exists
if (data._parse_succeeded === false && data._has_valid_data) {
return { type: 'parse_error', raw: data.raw_response }
}
// 3. Network failure (caught in try/catch)
}For UI bugs specifically, we also hook into Next.js error boundaries and window error events:
javascript
// Catch unhandled UI errors in the Next.js app
window.addEventListener('error', (event) => {
const issue = {
type: 'ui_error',
message: event.message,
stack: event.error?.stack,
component: extractComponentFromStack(event.error?.stack)
}
showErrorModal(issue)
})// Catch React rendering errors
window.addEventListener('unhandledrejection', (event) => {
// Handle async component failures
})The parse error case is the interesting one — the AI response is there, the UI just can’t read it correctly. UI bugs give us the component stack trace, so the AI knows exactly which component to fix.
The iframe bridge
When the user clicks “Fix with AI”, we beam structured context to the parent:
javascript
window.parent.postMessage({
type: 'FIX_ERROR_REQUEST',
source: 'architect-child-app',
payload: {
error: 'parse_error',
message: 'JSON parsing failed',
raw_response: data.raw_response,
fixPrompt: 'Update UI to extract response.result.answer'
}
}, '*')The parent AI receives this, understands exactly what broke, and regenerates the UI code with the fix applied.
Results for Lyzr Architect?
MetricBeforeAfterError recovery rate0% (manual refresh)~85% (one-click fix)User drop-off on errorsHighMinimalRepeat bugsPersistentSelf-correcting
Users no longer hit dead ends. Errors became feedback loops. The AI-generated app essentially became self-healing.
Architecture
Gotchas
- Don’t auto-fix — show users a modal with the error context and let them choose to fix. Keeps users in control.
- Clone before reading —
response.clone()is critical, otherwise you consume the response body and the UI gets nothing. - Structured error payloads — generic “error occurred” messages are useless for AI repair. Include the raw response, the expected format, and a suggested fix.
- Whitelist your intercepts — only catch your own API calls, not third-party requests.
- Next.js hydration errors — these are tricky. Include both server and client state in your error payload so the AI can spot the mismatch.
Honestly if your AI-generated apps are simple and stateless, you might not need this. But if you’re building an agent that generates interactive Next.js apps that talk to backends like we did for Lyzr Architect, this pattern turns dead ends into feedback loops.










