πŸ”§ Error Fixes
Β· 6 min read
Last updated on

Nuxt 3: useFetch Returns Null β€” How to Fix It


const { data } = await useFetch('/api/users');
console.log(data.value); // null β€” why?

Your Nuxt 3 useFetch call returns null for data even though you expect a response. This is one of the most common Nuxt 3 issues because useFetch behaves differently from a regular fetch β€” it runs during SSR, caches responses, and has specific rules about where and how it can be called.

How useFetch works

useFetch is a Nuxt composable that:

  1. Runs on the server during SSR (first page load)
  2. Serializes the response into the HTML payload
  3. On the client, hydrates from the payload instead of re-fetching
  4. Returns a reactive Ref β€” you access the value with .value

If any step in this chain fails, data will be null. The tricky part is that errors during SSR are often silent β€” the page renders without data and no error appears in the browser console.

Fix 1: Check that the API route exists and returns data

The most common cause is a missing or broken server route.

// ❌ server/api/users.ts doesn't exist or has an error
const { data } = await useFetch('/api/users'); // data is null

// βœ… Create the API route: server/api/users.ts
export default defineEventHandler(async (event) => {
  return [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
  ];
});

Verify it works: Visit http://localhost:3000/api/users directly in your browser. If you see JSON, the route works. If you get a 404 or error, fix the route first.

Common route issues:

  • File is in the wrong directory (must be server/api/ or server/routes/)
  • File has a TypeScript error that prevents compilation
  • The handler doesn’t return anything (implicit undefined)
  • The handler throws an error that gets swallowed

Fix 2: Destructure correctly and check error

useFetch returns an object with data, error, pending, and refresh. If you don’t destructure, you’re working with the wrapper object, not the data.

<script setup>
// ❌ Wrong β€” this is the entire composable return object
const response = await useFetch('/api/users');
console.log(response); // { data: Ref, error: Ref, pending: Ref, ... }

// βœ… Destructure properly
const { data, error, pending } = await useFetch('/api/users');

// βœ… Always check for errors
if (error.value) {
  console.error('Fetch failed:', error.value.message);
}
</script>

<template>
  <div v-if="pending">Loading...</div>
  <div v-else-if="error">Error: {{ error.message }}</div>
  <ul v-else-if="data">
    <li v-for="user in data" :key="user.id">{{ user.name }}</li>
  </ul>
</template>

Important: data is a Ref. In the script, access it with data.value. In the template, Vue auto-unwraps refs so you can use data directly.

Fix 3: External APIs blocked during SSR

When your Nuxt server tries to fetch an external API during SSR, it might fail because:

  • The API requires authentication headers that aren’t available server-side
  • The API blocks requests from server IPs
  • CORS doesn’t apply server-side, but the API might have IP allowlists
  • The API is unreachable from your deployment environment
// ❌ External API unreachable during SSR β€” data is null on first load
const { data } = await useFetch('https://api.example.com/users');

// βœ… Option 1: Skip SSR for this request
const { data } = await useFetch('https://api.example.com/users', {
  server: false, // Only fetches on client side
});

// βœ… Option 2: Proxy through your own API route
// server/api/users.ts
export default defineEventHandler(async () => {
  const response = await $fetch('https://api.example.com/users', {
    headers: { Authorization: `Bearer ${process.env.API_KEY}` },
  });
  return response;
});

// Then fetch your own route
const { data } = await useFetch('/api/users');

Fix 4: useFetch called outside setup context

useFetch must be called at the top level of <script setup> or inside setup(). Calling it inside callbacks, event handlers, or onMounted won’t work as expected.

<script setup>
// ❌ Called inside onMounted β€” won't participate in SSR
onMounted(async () => {
  const { data } = await useFetch('/api/users'); // May return null during hydration
});

// βœ… Call at the top level of setup
const { data } = await useFetch('/api/users');

// βœ… If you need to fetch on user action, use $fetch instead
async function loadMore() {
  const moreUsers = await $fetch('/api/users?page=2');
}
</script>

Fix 5: Key conflicts and caching

useFetch caches responses by URL. If you call useFetch with the same URL in multiple components, they share the same cached response. If the first call fails, subsequent calls also get null.

// ❌ Same key, different expectations
const { data: users } = await useFetch('/api/users');
const { data: admins } = await useFetch('/api/users'); // Returns cached result from above

// βœ… Use unique keys
const { data: users } = await useFetch('/api/users', { key: 'all-users' });
const { data: admins } = await useFetch('/api/users?role=admin', { key: 'admin-users' });

To force a fresh fetch, use refresh():

const { data, refresh } = await useFetch('/api/users');

// Later, force re-fetch
await refresh();

Fix 6: Response type mismatch

If your API returns a non-JSON response (HTML error page, empty body, or malformed JSON), useFetch will set data to null.

// ❌ API returns HTML error page instead of JSON
// data will be null, error will contain the parse failure

// βœ… Check what the API actually returns
const { data, error } = await useFetch('/api/users');
if (error.value) {
  console.error('Status:', error.value.statusCode);
  console.error('Message:', error.value.message);
  console.error('Data:', error.value.data); // Raw response body
}

Fix 7: Watching reactive parameters

When using reactive parameters, useFetch re-fetches when they change. But if the initial value is invalid, the first fetch returns null.

const userId = ref('');

// ❌ First fetch with empty string returns null/error
const { data } = await useFetch(() => `/api/users/${userId.value}`);

// βœ… Use watch option or conditional fetch
const { data } = await useFetch(() => `/api/users/${userId.value}`, {
  immediate: false, // Don't fetch until userId is set
});

// Fetch when userId is available
watch(userId, () => {
  if (userId.value) refresh();
});

Debugging checklist

  1. Check the error ref: console.log(error.value) β€” this often contains the real reason
  2. Test the API directly: Visit the URL in your browser or use curl
  3. Check Nuxt DevTools: The Payload tab shows what was fetched during SSR
  4. Check server logs: Errors during SSR appear in your terminal, not the browser
  5. Add onResponseError: Hook into the fetch lifecycle for debugging:
const { data } = await useFetch('/api/users', {
  onResponseError({ response }) {
    console.error('Response error:', response.status, response._data);
  },
  onRequestError({ error }) {
    console.error('Request error:', error);
  },
});

useFetch vs $fetch β€” when to use which

useFetch$fetch
SSR supportβœ… Runs on server, hydrates on client❌ Only runs where called
Reactiveβœ… Returns Refs, auto-updates❌ Returns plain data
Cachingβœ… Deduplicates and caches❌ No caching
Use in setupβœ… Requiredβœ… Works anywhere
Use in event handlers❌ Use $fetch insteadβœ… Ideal

FAQ

Why does useFetch work in development but not in production?

Common causes: environment variables not set in production, API URLs that differ between environments, or the API being unreachable from your production server. Check your runtimeConfig in nuxt.config.ts.

Can I use useFetch with POST requests?

Yes:

const { data } = await useFetch('/api/users', {
  method: 'POST',
  body: { name: 'Alice', email: 'alice@example.com' },
});

Why does data become null after navigation?

If you’re using keepalive: false (default) and navigate away then back, the component re-mounts and re-fetches. If the re-fetch fails, data is null. Use keepalive: true in your page meta or handle the loading state.

How do I type the response?

interface User {
  id: number;
  name: string;
}

const { data } = await useFetch<User[]>('/api/users');
// data.value is User[] | null