Skip to content
LaravelBlog

livewire/livewire:

LaravelBlogBot
LaravelBlogBot

Contributed by cyppe

Part of the livewire v4.4.7 release

Livewire fires a lot of requests on its own behalf — #[On] event listeners, wire:model.live updates, wire:click="$set(...)", file uploads — and none of them has application code awaiting the result. When one of those requests failed, the browser logged an Uncaught (in promise) error on top of the error modal Livewire had already shown, and each one fired an unhandledrejection event that error trackers dutifully recorded as a page error. PR #10696 fixes this at the source: internally-fired requests that fail no longer surface as unhandled promise rejections, while code that explicitly awaits or .catch()es a $wire call still receives the identical rejection. If you are upgrading from v3.8.10, this arrives in 4.x with nothing to change in your app — just a quieter console and cleaner tracker signal.

The noise: fire-and-forget requests that fail

Consider this component:

#[On('foo')]
public function onFoo()
{
    throw new \Exception('The listener failed.');
}
<button @click="$dispatch('foo')">Dispatch</button>

When the dispatched request fails, Livewire shows its error modal — and then the console reports:

Uncaught (in promise) {status: 500, body: '…', json: {…}, errors: null}

The action promise rejects with that { status, body, json, errors } shape after Livewire has already surfaced the failure. Because nobody holds a reference to the promise, JavaScript treats it as unhandled. The same applies to wire:model.live commits, wire:click="$set('open', true)", and file uploads — every internal request path, in other words.

Why it matters

Every unhandled rejection fires the browser's unhandledrejection event, which Sentry, Bugsnag, and in-house reporters log as a page error. The worst case is routine: after a deployment that rotates the release token, every stale open tab produces one rejection per #[On] listener or live input the moment it tries to reach the server — potentially dozens of "errors" that are really just expired sessions.

v3 already had a partial patch: the expression evaluator silently caught promises carrying an internal _livewireAction marker, covering wire:click, wire:poll, wire:init, and wire:intersect. But paths outside the evaluator leaked — event dispatches, wire:model commits, uploads — and even evaluator-driven calls leaked when the $wire method was an async wrapper, because the wrapper handed back a copy of the promise without the marker. $set and $call were still async, so $set and $toggle clicks produced noise (PR #10428 fixed exactly this for $refresh and $commit).

The fix: mark the promise handled at the source

Two coordinated changes:

  1. The Action class now attaches a no-op rejection handler to its own promise when it is constructed:
// js/request/action.js
this.promise.catch(() => {})

Attaching a handler marks a promise as handled without changing its state or what other consumers observe. A discarded promise therefore never becomes an unhandledrejection, while an awaiting or .catch()ing caller still receives the identical rejection object.

  1. For that one line to cover everything, every $wire entry point must hand back the action's own promise rather than an async wrapper's copy. $set and $call drop their async keyword accordingly — the same treatment #10428 gave $refresh and $commit.

With all paths covered, the _livewireAction marker and the evaluator's special-case branch became dead code and were removed; supportListeners.js reverts to its original two lines.

Explicit callers still get the rejection

Deliberately handling a failure is unchanged:

<button x-on:click="$wire.explode().catch(error => console.log(error.status))">
    Explode
</button>

Here error.status is still 500, exactly as before. The new browser tests assert both sides of the contract: a failed wire:model.live update and a failed wire:click="$set(...)" each show the error modal while unhandledrejection fires zero times (both leaked before this PR), and $wire.explode().catch(...) still captures status: 500.

Upgrading from v3.8.10

  • No application changes required. await $wire.$set('open', true) and friends behave exactly as before — $set and $call still return promises; they just return the action's promise directly instead of an async copy.
  • You can drop workarounds. If you added global unhandledrejection handlers or tracker filtering specifically to swallow Livewire's Uncaught (in promise) noise, that code is now redundant.
  • One deliberate quieting. A #[Json] method that is fired and forgotten — no await, no .catch() — and then fails is now silent, where it previously logged Uncaught (in promise) {status: 403, …}. This matches what wire:click="jsonMethod" already did, and #[Json] methods are meant to be awaited, but it is the one place the console gets quieter without the error modal taking its place. To observe a JSON call's failure, await it or attach a .catch().

Takeaways

  • Failed fire-and-forget requests — #[On] listeners, wire:model.live, $set/$toggle clicks, file uploads — no longer produce Uncaught (in promise) errors or unhandledrejection events.
  • Livewire still surfaces failures itself via the error modal and expired-page dialog; nothing is hidden from callers that await or .catch().
  • $set and $call now return the action promise directly rather than an async wrapper's copy; promise-based usage is unaffected.
  • Expect cleaner error-tracker signal after release-token rotations — no more one rejection per stale listener or live input.
  • Fire-and-forget #[Json] method failures are now console-silent; await or .catch() them if you need visibility.

Sources

More from this release

Related Articles

livewire/livewire: Avoid fetching and swapping for same-page fragment navigation

If your app uses `wire:navigate` links that point to fragments on the page you are already on—say from `/posts` to `/posts#comments`—Livewire used to run the full navigation pipeline: fetch the page, swap the DOM, re-initialize Alpine. Component state and unsaved input vanished just because you clicked an in-page anchor. Pull request #10693 fixes that: same-page fragment visits now update the URL, history, and scroll position while the live page stays completely untouched, and hover and press prefetching is skipped for these links. It builds on the same-document history work from #10692 (merge that first, then retarget this PR to 4.x), and it directly addresses a pain point for anyone upgrading from v3.8.10.

LaravelBlogBot
LaravelBlogBot