How to Solve the Nuxt.js Composable Access Error (TypeScript)
While working on a Nuxt.js 3 project with Supabase authentication, I ran into a confusing error that had me scratching my head for longer than I’d like to admit.
I was trying to refresh a session via a composable, then use it inside middleware which should be valid… except Nuxt yelled at me:
[nuxt.js] A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function.
Let me walk you through the issue, the cause, and how I fixed it plus a few best practices to make your middleware even more reliable.
The Problem Invalid Composable Structure
Here’s the original code that triggered the issue.
/middleware/org.ts
export default defineNuxtRouteMiddleware(async () => {
const newSession = await useNewSession() // ERROR TRIGGERS HERE
const accessToken = newSession.access_token
const { data, error } = await useFetch('/api/organization', {
headers: {
authorization: `Bearer ${accessToken}`
}
})
if (error.value) {
throw error
}
const organizationCookie = useCookie('organization')
organizationCookie.value = JSON.stringify(data.value)
})
/composables/useNewSession.ts
export default async function() {
const supabase = useNuxtApp().$supabase
const { data, error } = await supabase.auth.refreshSession()
if (error || !data.session) {
navigateTo('/login')
const toast = useToast()
toast.add({
title: 'Session error',
description: error?.message,
color: 'error',
})
throw error
}
return data.session
}
Why Nuxt Throws This Error
Even though I called the function inside middleware, Nuxt.js still rejected it because:
- A valid composable must:
- Start with
use... - Be a pure function, not something that triggers navigation or toasts internally
- Preferably be synchronous in structure, even if it returns a Promise
- Start with
Because I exported async function () { ... } as default, Nuxt didn’t treat it as a composable it treated it like a regular utility function, which caused the warning.
The Fix: Make It a Proper Composable & Move Logic to Middleware
Here’s how I rewrote it.
/composables/useNewSession.ts (Fixed)
export const useNewSession = async () => {
const supabase = useNuxtApp().$supabase
const { data, error } = await supabase.auth.refreshSession()
return { session: data?.session, error }
}
/middleware/org.ts (Improved)
export default defineNuxtRouteMiddleware(async () => {
const { session, error } = await useNewSession()
if (!session || error) {
const toast = useToast()
toast.add({
title: 'Session expired',
description: 'Please log in again.',
color: 'error',
})
return navigateTo('/login')
}
const accessToken = session.access_token
const { data, error: orgError } = await useFetch('/api/organization', {
headers: { authorization: `Bearer ${accessToken}` }
})
if (orgError.value) throw orgError
useCookie('organization').value = JSON.stringify(data.value)
})
Now everything works perfectly no warnings, clean logic separation, and a much more reusable pattern.
Bonus Improvements I Plan to Add
| Feature Idea | Description |
|---|---|
| Token expiry check | If the access token expires in less than X minutes, refresh proactively |
| Retry logic | If /api/organization fails once, try again before throwing |
| Session timestamp logging | Store last refresh time in localStorage or cookie |
| Global loading indicator | Show a spinner while middleware is running on route change |
Final Thoughts
This error wasn’t really about where I used the composable it was about how I structured it. Nuxt.js expects composables to stay pure and free of side effects, and once I moved navigation and UI handling out of the composable and into the middleware, everything fell into place. A small refactor made my code cleaner, more reusable, and much easier to maintain. If you hit this issue too, don’t worry it’s an easy fix once you understand the pattern.