TL;DR: After weeks in a production Laravel 12 + React 19 + Inertia v2 app, I repeatedly hit failure modes that were expensive to diagnose: overlapping visit cancellation, deploy-time stale chunk breakage, weak default failure UX, and framework-specific workaround code. This article is blunt, but scoped: these are observed behaviors in a real stack, backed by docs/issues where available.
Scope (What This Is, What This Isn't)
This is not a claim that Inertia fails in every project. Plenty of teams run Inertia successfully for CRUD-heavy admin apps.
This is a claim that in one real production setup with active users and frequent deploys, Inertia's router abstraction created recurring operational pain and non-obvious failure patterns.
Environment referenced throughout:
- Laravel 12
- React 19
- Inertia.js v2
- Vite code splitting
- Replace-in-place style deployments in some environments
The Pitch vs. The Reality
Inertia's core pitch is strong: build SPA-like UX without maintaining a separate public API surface for routine web navigation.
The trouble started when workflows became non-trivial: multi-step actions, deployment churn, and edge-case error handling.
1. Sequential Request Pitfall: await Does Not Serialize Inertia Router Visits
In our app, assigning a worker required two ordered operations:
const handleAssign = async () => {
// Step 1: Assign worker
await router.put(`/admin/tasks/${task.id}/assign`, {
assignee_id: Number(selectedUserId)
})
// Step 2: Update status
await router.put(`/admin/tasks/${task.id}/status`, {
status: 'In Progress'
})
setModalOpen(false)
}
With Promise-based clients (fetch, axios), that shape means strict sequencing.
In our case, observed outcome was:
- status updated
- assignment did not
- first request showed cancelled in Network
- no obvious app-level error surfaced by default
Why this can happen:
- Inertia router methods are not Promise-returning in the way this code assumes
awaittherefore doesn't guarantee request completion order- overlapping visits can cancel previous visits (by design)
Community discussions: Promise support intentionally removed, years of requests for it.
A working pattern was callback chaining:
const handleAssign = () => {
router.put(`/admin/tasks/${task.id}/assign`, {
assignee_id: Number(selectedUserId)
}, {
onSuccess: () => {
router.put(`/admin/tasks/${task.id}/status`, {
status: 'In Progress'
}, {
onSuccess: () => setModalOpen(false)
})
}
})
}
Or manually wrapping visits in a Promise:
await new Promise((resolve, reject) => {
router.patch(route('profile.update'), data, {
onSuccess: resolve,
onError: reject,
})
})
This is exactly where frustration spikes: code that looks like normal async/await HTTP is not normal async/await HTTP.
2. Deploy-Time Stale Chunks: Not Unique to Inertia, But Operationally Sharper with Server-Client Coupling
Any code-split SPA can suffer stale chunk issues after deploy. This is not Inertia-exclusive.
Inertia made impact broader in our setup because navigation depends on server-side component resolution plus client-side chunk import.
Representative chunk names:
assets/bookings-show-A3f8kQ2.js
assets/profile-Bp7mXn1.js
assets/schedule-Ck9pLw4.js
After deploy:
- server references latest component manifest
- client tab may still hold older runtime assumptions
- needed chunk import fails if asset no longer available
- user perceives "dead" navigation until hard reload
Nuance that matters:
- Immutable artifact / skew-protected platforms reduce impact.
- Replace-in-place deployments increase risk window.
- Cache and rollout strategy matters as much as framework choice.
References: Inertia asset versioning / 409, 409 loop report.
Important precision:
- I am not claiming every deploy kills every tab in all environments.
- I am claiming this was a repeated production incident pattern in our environment.
3. Failure UX Defaults to Silence
In our app, we added explicit guardrails to make failures visible/recoverable.
// Catch navigation exceptions and force reload
router.on('exception', (event) => {
event.preventDefault();
window.location.href = event.detail.url || window.location.href;
});
// Proactive manifest drift check
let manifest: string | null = null;
fetch('/build/manifest.json')
.then(res => res.text())
.then(text => { manifest = text; })
.catch(() => {});
document.addEventListener('visibilitychange', async () => {
if (document.visibilityState !== 'visible' || !manifest) return;
try {
const res = await fetch('/build/manifest.json', { cache: 'no-store' });
if (await res.text() !== manifest) window.location.reload();
} catch {}
});
These mitigations worked. They are also framework-specific operational debt you must know to write.
4. Navigation Errors Vanish Without a Trace
When a JavaScript error occurs in a target page component, navigation fails silently. The previous page stays visible. No error message, no console warning, no loading indicator that stops. The user clicks a link, waits, and nothing happens.
When server errors occur, Inertia's default behavior is to render the entire error response inside a modal overlay. In development, that's the full Laravel debug page in a modal on top of your app. In production, it's a generic HTML error page — still in a modal, still bizarre UX. To fix it, you override the exception handler to return JSON, then catch it client-side with toast notifications. More workaround code.
In the codebase I work with, I found both router.reload() and window.location.href used for navigation — the latter being a sign the developers gave up on Inertia's router for certain flows. That split can be rational, but it also means engineers must learn two interaction patterns.
5. Props in HTML: Not Unique, Still a Real Discipline Requirement
This is not an Inertia-only security story. Any client-delivered data is visible client-side.
Still, with Inertia, props serialized into data-page make over-sharing easy if teams are careless.
References: props visible in page source, cached sensitive data after logout.
Defensible statement: treat every prop as public output; never include data you would not expose in client payloads.
6. "No API" Is Better Framed as a Starting Optimization, Not a Permanent Architecture
The marketing line can be useful early: fewer moving parts for web navigation.
In many real systems, teams still add explicit API endpoints for:
- third-party integrations and webhooks
- mobile clients
- background workflows
- specialized, strongly ordered interactions
Important correction for accuracy:
- Inertia supports file uploads and
FormDatapatterns. - Our team still used direct
fetch()in some upload paths for local reliability/control reasons. - That is a project-level tradeoff, not proof that Inertia cannot upload files.
The Root Problem (In This Stack)
The recurring cost was semantic mismatch:
- code looked like normal Promise-based HTTP flow
- runtime behavior followed router-visit semantics
- failure surfaced under production conditions, not in happy-path demos
That mismatch consumed debugging time and required defensive patterns beyond what most developers expect from "simple SPA routing."
The Alternative We Prefer in High-Complexity Flows
For critical ordered operations, explicit HTTP was easier to reason about:
const handleAssign = async () => {
await fetch(`/api/tasks/${task.id}/assign`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ assignee_id: Number(selectedUserId) })
})
await fetch(`/api/tasks/${task.id}/status`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'In Progress' })
})
setModalOpen(false)
}
This is not about fewer lines. It's about predictable behavior, standard tooling expectations, and portability across backends.
The Honest Take
I am frustrated with this framework because these incidents were real and costly. The request cancellation bug consumed a full day of debugging. The deploy issue cost another afternoon. Each was solvable — but with framework-specific defensive code that shouldn't need to exist.
The defensible conclusion is not "never use Inertia." Plenty of Laravel admin panels and internal tools run it without issues.
It is: if your system has multi-step interactions, active-user deploy churn, and strict operational reliability needs, evaluate whether explicit API + standard HTTP client semantics lower your long-term risk. In our case, the answer was unambiguous.
I build MVPs at CodeCrank. If you're evaluating tech stacks for your next project, let's talk.