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?
answer
- one Nuxt app per request
- the current app is tracked implicitly
- context lost after an await
- the browser holds a singleton
- runWithContext restores it
basics
~20 suseNuxtApp() 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// 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
Recall that useNuxtApp() gives access to the app's payload, provided helpers and hooks, and that composables belong in setup, plugins or middleware.
Explain why Nuxt tracks a current app, which call sites restore it after an await, and what the payload and ssrContext contain.
Diagnose SSR-only NUXT_E1001 errors in async helpers and fix them by ordering calls or using runWithContext, not by scattering workarounds.
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