Skip to content
Release notesNuxt

Nuxt 4.2: abort signals in useAsyncData and a new error overlay

Nuxt 4.2 was released on 25 October. The headline feature is one extra argument in your data handlers: pass it to $fetch, and a refresh, a clear() or a timeout cancels the pending request instead of letting it finish.

vuejs.school team30 Oct 20256 min readNuxt 4.2

Nuxt 4.2 is a minor release, published on 25 October, the day of Vue Fes Japan. Nothing breaks on purpose. The release post leads with two changes you will meet in daily work: useAsyncData handlers receive an abort signal that can cancel a request on the network, and development shows your own error page and the stack trace together.

If your pages load data with useAsyncData and $fetch, the signal is the change to understand before you upgrade. The examples use the Nuxt 4 layout: pages/ and components/ live inside app/, and server/ stays at the project root.

What shipped in Nuxt 4.2

The release post lists these items:

  • useAsyncData handlers receive an AbortSignal, so refresh() and clear() can cancel the request in flight.
  • In development, your custom error page and a technical error overlay with the stack trace are shown together.
  • An opt-in, experimental Vite Environment API integration behind experimental.viteEnvironmentApi.
  • The Nitro server integration moved into its own package, @nuxt/nitro-server, as an internal refactor.
  • Renderer dependencies are computed at build time rather than on each cold start, and the kit and schema packages dropped dependencies they did not need.
  • An experimental option, experimental.extractAsyncDataHandlers, that moves data handlers into separate chunks for prerendered sites.
  • Experimental TypeScript plugin support behind experimental.typescriptPlugin.

Of the rest, the new package and the speed-ups need nothing from you, and the three experimental options stay off until you turn them on.

Smaller items: a declarationPath option for components, an extensions option for the kit's resolveModule, a setGlobalHead utility for module authors, and fixes for the route hash being dropped on routeRules redirects, object-form href in <NuxtLink>, auto-imported components passed to h(), and array handling in app config during hot reload.

Pass the abort signal to $fetch

Before 4.2, dedupe: 'cancel', the default for useAsyncData, meant "ignore the result of the previous request". When a refresh replaced a pending request, the HTTP request itself kept running to the end. Now the handler receives a signal as its second argument, and when you pass it on to $fetch, the replaced request is aborted for real:

app/pages/users.vue
<script setup lang="ts">
type User = { id: number; name: string }

const { data: users, status } = await useAsyncData('users', (_nuxtApp, { signal }) =>
  $fetch<User[]>('/api/users', { signal }),
)
</script>

<template>
  <p v-if="status === 'pending'">Loading…</p>
  <ul v-else>
    <li v-for="user in users" :key="user.id">{{ user.name }}</li>
  </ul>
</template>

The handler's first argument is still the Nuxt app instance, which most handlers ignore. The second is an object with signal. $fetch accepts a signal option like the native fetch does, so passing it through is the whole change. The handler type is now (nuxtApp, { signal }) => Promise<T>, but a handler that only declares nuxtApp, or nothing at all, still type-checks and still runs.

The handler's signal is aborted in four cases:

  • A new request for the same key starts while one is pending (a refresh(), an execute() or a change to a watched value), under the default dedupe: 'cancel'.
  • You call clear().
  • The request is still pending when the new timeout option, given in milliseconds, runs out.
  • A signal of your own, passed to refresh(), is aborted (next section).

A cancelled request is not an error. When a newer request replaces it, the newer one sets data and status. When clear() or your own signal cancels it, Nuxt catches the AbortError, leaves error empty and sets status back to 'idle'. A timeout is the exception: it ends with a TimeoutError, so error is set and status becomes 'error', like any failed request.

useFetch builds on useAsyncData. It already aborted a replaced request before 4.2, with an AbortController of its own, and it already had a timeout option. In 4.2 it passes Nuxt's signal to $fetch instead, so the four cases above apply to useFetch too, without any change to your code.

Cancel a refresh with your own AbortController

refresh() and execute() accept a signal too, which lets a user, or a component's lifecycle, cancel a request. This component loads a report only when asked (immediate: false) and offers a Cancel button:

app/components/ReportPanel.vue
<script setup lang="ts">
const { data: report, status, refresh } = await useAsyncData(
  'report',
  (_nuxtApp, { signal }) => $fetch('/api/report', { signal }),
  { immediate: false },
)

let controller: AbortController | undefined

function run() {
  controller = new AbortController()
  return refresh({ signal: controller.signal })
}

function cancel() {
  controller?.abort()
}

onBeforeUnmount(cancel)
</script>

<template>
  <button type="button" :disabled="status === 'pending'" @click="run">Run report</button>
  <button type="button" :disabled="status !== 'pending'" @click="cancel">Cancel</button>
  <pre v-if="report">{{ report }}</pre>
</template>

Nuxt merges the controller's signal with its own internal one, so the handler still receives a single signal. When the user clicks Cancel, the request is aborted, Nuxt catches the AbortError, and status returns to 'idle', which re-enables the Run button.

Nuxt does not cancel a pending request when its component unmounts, so onBeforeUnmount(cancel) stops a slow report when the user leaves.

Your error page and the stack trace in development

Before 4.2, a custom error.vue was also what you saw in development, so the stack trace you need to find the cause stayed hidden unless your page printed it. In 4.2, when a request fails on the server during development, Nuxt renders your error page and lays a technical overlay over it with the stack trace and debugging details.

A small preview in the bottom-right corner switches between the two views: click it to see the page your users would get, and click the corner again to bring the overlay back. Escape also hides the overlay. It is added only in development and never ships to production.

Three experimental options to try in a branch

experimental.typescriptPlugin: true installs and configures the @dxup/nuxt module, which adds TypeScript language-service plugins for Nuxt: renaming an auto-imported component file updates its references, go to definition works on dynamic glob imports and on runtime config properties, and a $fetch('/api/users') call jumps to its handler in server/api. In VS Code the plugins need the workspace version of TypeScript: run "TypeScript: Select TypeScript Version" and choose "Use Workspace Version".

experimental.extractAsyncDataHandlers: true moves the functions you pass to useAsyncData and useLazyAsyncData into separate chunks that load on demand. On a prerendered site the handler code is only needed at build time, so the client bundle shrinks; the post reports 39% less JavaScript on an earlier version of nuxt.com and says results vary.

experimental.viteEnvironmentApi: true lets one Vite dev server serve the client and server builds as two environments instead of running two servers. The post marks it highly experimental and calls it the first breaking change toward the next major version of Nuxt; setting future.compatibilityVersion: 5 opts into it and into future breaking changes, and the post says to do that for testing only.

Upgrade, then add the signal to your handlers

One command refreshes the lockfile and the dependencies Nuxt relies on:

npx nuxt upgrade --dedupe

The upgrade alone changes nothing on the network for useAsyncData: a handler that ignores the signal still lets a replaced request run to the end. After upgrading, go through your handlers:

  1. In each useAsyncData handler that calls $fetch, add { signal } as the second parameter and pass it to the request.
  2. Leave it out where the request changes something on the server, for example a POST that sends an email. With the signal, a refresh aborts that request mid-flight, and the server may see a closed connection. To keep the signal there, pass dedupe: 'defer', so a refresh reuses the pending request instead of replacing it, or stop refreshing while it is pending.

Upgrade now. The signal makes dedupe: 'cancel' do what its name says, the overlay gives you the stack trace without hiding your own error page, and nothing else in the list requires a change to your code.

Check it stuck3 questions from the Data fetching quiz.
Practice now
Was this helpful?

Keep reading