skip to content

In Nuxt 4, what does useNuxtApp() give a composable, and why can a Nuxt composable called after an await throw NUXT_E1001 during SSR only?

level: seniorimportance: should knowfreq 31%

answer

  1. one Nuxt app per request
  2. the current app is tracked implicitly
  3. context lost after an await
  4. the browser holds a singleton
  5. runWithContext restores it

basics

~20 s

useNuxtApp() returns the current Nuxt app: vueApp, provided helpers, hooks, payload, ssrContext and isHydrating. On the server each request has its own app tracked implicitly; after an await in a plain function that tracking is lost, so the next Nuxt composable throws NUXT_E1001.

solid answer

~40 s

`useNuxtApp()` returns the Nuxt app instance: `vueApp`, helpers added with `provide` (as `$name`), runtime hooks, the `payload` with `state` and fetched `data`, the server-only `ssrContext`, `isHydrating`, and `runWithContext`. On the server, Nuxt creates one app per request and tracks which one is current, so composables like `useState` need no request argument. That tracking holds while code runs synchronously from `setup`, a plugin or middleware. Nuxt's compiler preserves it across `await` in `<script setup>`, `defineNuxtComponent`, plugins and middleware, but not in a plain helper. After an `await` there, `useState` calls `useNuxtApp()`, finds no app, and throws `NUXT_E1001`. In the browser there is a single app set globally, so the same code usually works, which is why it fails only during SSR. The fixes: call composables before the `await`, or capture `nuxtApp` first and use `nuxtApp.runWithContext()`.

code

ts · 12 lines
ts
// app/composables/counter-sync.ts
export async function syncCounter () {
  // call composables before the first await
  const nuxtApp = useNuxtApp()
  const counter = useState('counter', () => 0)

  const { value } = await $fetch<{ value: number }>('/api/counter')
  counter.value = value

  // or, if a composable is needed after the await:
  await nuxtApp.runWithContext(() => refreshNuxtData())
}

go deeper

for a junior

Recall that useNuxtApp() gives access to the app's payload, provided helpers and hooks, and that composables belong in setup, plugins or middleware.

for a middle

Explain why Nuxt tracks a current app, which call sites restore it after an await, and what the payload and ssrContext contain.

for a senior

Diagnose SSR-only NUXT_E1001 errors in async helpers and fix them by ordering calls or using runWithContext, not by scattering workarounds.

for a principal

Set conventions for async composables so request isolation holds by design, and decide whether experimental async context is worth adopting.

## What useNuxtApp returns `useNuxtApp()` returns the **Nuxt app instance**, the object every Nuxt composable uses under the hood: | Member | What it gives you | |---|---| | `vueApp` | the Vue application: `component()`, `directive()`, `use()` | | `provide(name, value)` and `$name` | helpers injected by plugins, such as `nuxtApp.$hello` | | `hook`, `callHook` | runtime hooks such as `page:start` or `vue:error` | | `payload` | server-to-client data: `state` from `useState`, `data` from the fetch composables, `serverRendered` | | `ssrContext` | server only: the request `url`, the h3 `event`, the payload | | `isHydrating` | `true` while the browser is hydrating server HTML | | `runWithContext(fn)` | runs `fn` with this app set as the current one | It is not available inside Nitro server routes, which work from the request event instead. ## Why there is a current app at all On the server, Nuxt creates **one app per request**, and many requests are in flight at once. Composables such as `useState` take no argument saying which request they belong to, so Nuxt tracks the current app implicitly, the way Vue tracks the current component during `setup`. This implicit context is what keeps one visitor's state out of another visitor's response. ## Why an await breaks it on the server The tracking is reliable only while code runs **synchronously** from a point where Nuxt set it: 1. A component's `setup`, a plugin or a route middleware starts, and Nuxt marks its app as current. 2. A helper runs `await $fetch('/api/counter')`. The function suspends, and work for other requests runs. 3. When it resumes, nothing guarantees the context still points at this request's app. 4. The next `useState('counter')` calls `useNuxtApp()`, finds no app, and throws **`NUXT_E1001`**: a composable that needs the Nuxt instance was called outside a plugin, hook, middleware or `setup`. In the **browser** there is only one app, and Nuxt keeps it set as a global singleton, so the same code usually works there. That is why this bug tends to appear only in server rendering. ## Where awaiting is safe Nuxt's compiler transforms `<script setup>`, the `setup` of components defined with `defineNuxtComponent`, `defineNuxtPlugin` and `defineNuxtRouteMiddleware`, restoring the context after each `await`. A plain function called from them gets no such help, even when it lives in `app/composables/`. ## Fixing the helper - **Call composables first, await later.** Take `const counter = useState('counter', () => 0)` at the top, then await, then write `counter.value`. - **Capture the app and re-enter it.** Store `const nuxtApp = useNuxtApp()` before the `await`, then call `nuxtApp.runWithContext(() => useState('counter'))` after it. - **Probe instead of throwing.** `tryUseNuxtApp()` returns `null` when no app is active, for helpers that can work without one. - **Opt into async context.** The experimental `experimental.asyncContext` flag lets Nuxt composables be used in async functions. ## Using the app's members in composables Most code never needs `useNuxtApp()` directly, because the composables wrap it. The cases where a composable reaches for it: - **Provided helpers.** A plugin that calls `nuxtApp.provide('counterApi', api)` makes `useNuxtApp().$counterApi` available to every composable; wrapping that access in `useCounterApi()` keeps call sites tidy. - **Hydration-aware work.** `nuxtApp.isHydrating` is `true` during the first client render, so a composable can skip work whose result is already in the server HTML. - **Payload inspection.** `nuxtApp.payload.serverRendered` tells whether the current page came from the server, and `payload.state` shows the `useState` entries while debugging. - **Hooks.** `nuxtApp.hook('page:finish', ...)` lets a composable react to navigation lifecycle events. ## Common misuses - Calling `useNuxtApp()` at a module's top level, where no request exists yet. - Using `nuxtApp.payload` as a general-purpose store instead of `useState`, which gives keys, typing and initialisers. - Reaching for `runWithContext` everywhere instead of ordering calls correctly; the docs ask to use it sparingly. - Reading `ssrContext` in code that also runs in the browser without an `import.meta.server` guard.

  • Why does Nuxt track the current app implicitly instead of making every composable take the app as an argument?
    So composables keep Vue's call-it-in-setup ergonomics while staying request-safe on the server. The price is the rule that composables run synchronously within a known context; explicit passing would avoid it but would thread the app through every function signature.
  • When would you use tryUseNuxtApp() rather than useNuxtApp()?
    In code that may run with or without a Nuxt app, such as a utility shared with tests or a function that can fall back to defaults. `tryUseNuxtApp()` returns `null` instead of throwing `NUXT_E1001`, so the caller can branch. For code that needs the app, `useNuxtApp()`'s error is the more useful signal.

saying these in an interview costs you the question

  • Nuxt composables work anywhere once the app has started
  • An await inside script setup always loses the Nuxt context
  • The E1001 error means Nuxt itself has a bug
  • tryUseNuxtApp() recreates a lost context
  • The browser and server share one Nuxt app instance
  • ssrContext is available in the browser after hydration