Inertia.js Silently Breaks Your App

Published: (February 16, 2026 at 12:12 PM EST)
7 min read
Source: Dev.to

Source: Dev.to

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
  • framework‑specific workaround code

Note: This is not a claim that Inertia fails in every project. Plenty of teams run Inertia successfully for CRUD‑heavy admin apps.
The claim is 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

ComponentVersion
Laravel12
React19
Inertia.jsv2
BundlerVite (code‑splitting)
DeployReplace‑in‑place style deployments in some environments

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️⃣ 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, the observed outcome was:

  • status updated
  • assignment did not
  • first request showed cancelled in Network tab
  • no obvious app‑level error surfaced by default

Why this can happen

  • Inertia router methods are not Promise‑returning in the way the code assumes.
  • await therefore doesn’t guarantee request‑completion order.
  • Overlapping visits can cancel previous visits (by design).

Community discussions: Promise support was intentionally removed after years of requests.

Working patterns

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)
      })
    }
  })
}

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️⃣ Stale Chunk Issues After Deploy

Any code‑split SPA can suffer stale‑chunk issues after a deploy – this is not Inertia‑exclusive.
Inertia made the impact broader 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

What happens after a deploy

  1. Server references the latest component manifest.
  2. A client tab may still hold older runtime assumptions.
  3. Needed chunk import fails if the asset is no longer available.
  4. User perceives a “dead” navigation until a hard reload.

Nuance that matters

  • Immutable‑artifact / skew‑protected platforms reduce impact.
  • Replace‑in‑place deployments increase the risk window.
  • Cache and rollout strategy matter 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.

Guardrails we added

// 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 = 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, but they are framework‑specific operational debt you must know to write.

3️⃣ Silent Navigation Failures on JavaScript Errors

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.

Server‑side errors

Inertia’s default behaviour is to render the entire error response inside a modal overlay:

  • Development: full Laravel debug page in a modal on top of your app.
  • Production: a generic HTML error page — still in a modal, still a bizarre UX.

Fix: override the exception handler to return JSON, then catch it client‑side with toast notifications (more workaround code).

4️⃣ Mixed Navigation Strategies

In the codebase I work with, I found both router.reload() and window.location.href used for navigation.

  • The latter is a sign 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️⃣ Prop Over‑Sharing (Security)

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.

Closing Thoughts

  • The marketing line can be useful early: fewer moving parts for web navigation.
  • In many real systems, teams still add explicit API layers or fallbacks because of the operational debt described above.

If you’re considering Inertia for a production SPA, be aware of:

  1. Visit cancellation semantics – don’t rely on await.
  2. Deploy‑time chunk drift – implement manifest checks or use immutable deployments.
  3. Default error UX – plan to override it.
  4. Navigation consistency – pick one router strategy and stick with it.
  5. Prop hygiene – assume everything sent to the client is public.

Understanding these patterns up‑front can save weeks of debugging and help you decide whether Inertia’s trade‑offs fit your project’s operational constraints.

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 trade‑off, not proof that Inertia cannot upload files.

The recurring cost was a semantic mismatch:

  1. Code looked like a normal Promise‑based HTTP flow.
  2. Runtime behavior followed router‑visit semantics.

The 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.”

Why explicit HTTP was easier for critical ordered operations

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 of code.
  • It’s about predictable behavior, standard tooling expectations, and portability across backends.

Frustrations with the framework

  • Request‑cancellation bug → consumed a full day of debugging.
  • Deploy issue → cost another afternoon.

Both were solvable, but required framework‑specific defensive code that ideally shouldn’t be necessary.

Takeaway

The defensible conclusion is not “never use Inertia.”
Many Laravel admin panels and internal tools run it without issues.

What matters:

  • If your system has multi‑step interactions, active‑user deploy churn, and strict operational reliability needs, evaluate whether an explicit API + standard HTTP client semantics lower your long‑term risk.

In our case, the answer was unambiguous.

About me

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

0 views
Back to Blog

Related posts

Read more »