Inertia.js Silently Breaks Your App

And You Won't Know Until Production

Back to Blog

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
  • await therefore 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 FormData patterns.
  • 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.


I build MVPs at CodeCrank. If you're evaluating tech stacks for your next project, let's talk.