
Confused about when to use $fetch, useFetch, or useAsyncData in Nuxt? This guide breaks down their differences, best use cases, and common pitfalls to help you make the right choice for your project.

Get notified when we release new tutorials, lessons, and other expert Nuxt content.
"Should I use $fetch, useFetch, or useAsyncData here?"
If you've built anything with Nuxt, you've probably asked yourself this question.
These three data-fetching tools might seem similar at first glance, but choosing the wrong one can lead to performance issues, unnecessary API calls, DX weirdness, or SSR headaches.
In this guide, I will demystify when and why to use each option, complete with some practical examples and common pitfalls to avoid. You'll learn:
First off, we’ll quickly take a look at their differences.
We have three approaches to retrieving data in Nuxt:
$fetch — A low-level HTTP client that runs on both the server and the client, powered by ofetch.useAsyncData — An SSR composable for more advanced operations and transformations.useFetch — A simple wrapper over useAsyncData and $fetch to simplify the most common use cases.These are similar but aim to solve different scenarios. Here’s a quick table:
Let me share with you some examples.
| Tool | Key Use Case | SSR Handling | Typical Scenario |
|---|---|---|---|
$fetch | Direct HTTP client (server or client) | Server: Skips a real HTTP round trip for internal Nuxt endpoints. Client: Normal network call, improved with interceptors and auto retries. | Server routes, or client-side event-driven actions |
useAsyncData | Complex SSR tasks (multi-fetch, transformations) | Fetches once on the server, then hydrates to the client, so no double-fetching | Any async method can be used with this composable, so the opportunities are endless |
useFetch | Combines $fetch and useAsyncData together | Uses useAsyncData so we get the same benefits | Fetching data from a URL, such as a product list or blog posts page |
Here are a few scenarios to help illustrate when and why we might want to use each of these data fetching methods.
The useFetch composable is the best choice to load initial data once on the server. It prevents extra calls after hydration.
Suppose we want to display a list of blog posts:
<script setup>
const { data: posts } = await useFetch('/api/posts')
</script>
<template>
<div>
<h1>My Posts</h1>
<div v-for="post in posts" :key="post.id">{{ post.title }}</div>
</div>
</template>
Here, we're fetching the data once on the server, then the client hydrates the data from the Nuxt payload, preventing extra calls.
Using useFetch instead of useAsyncData just provides a simpler interface. Here’s how it would look if we used useAsyncData instead:
<script setup>
const { data: posts } = await useAsyncData(() => {
return $fetch('/api/posts');
})
</script>
<template>
<div>
<h1>My Posts</h1>
<div v-for="post in posts" :key="post.id">{{ post.title }}</div>
</div>
</template>
The $fetch method is more direct for forms or user-triggered actions. It doesn’t store SSR data in the Nuxt payload and doesn’t automatically prevent re-fetching.
But on the client, it’s great for anything triggered by a user event, such as button clicks or searching, or anything like that. Here, we’re creating a new contact following a form submission:
async function submitForm() {
if (!formData.value) return
const response = await $fetch('/api/contact', {
method: 'POST',
body: formData.value,
});
formData.value = null;
}
It's great for event-driven actions like form submissions or button clicks or anything like that.
The useAsyncData composable lets us use whatever async operation we want and have it work with SSR. Because of this, it also gives us concurrency control or custom transformations, or really anything you can stick in an async function.
For example, we can fetch multiple endpoints in parallel and then transform the results:
<script setup>
const { data, error } = await useAsyncData('dashboard', async () => {
const [user, stats, notifications] = await Promise.all([
$fetch('/api/user'),
$fetch('/api/stats'),
$fetch('/api/notifications')
])
return { user, stats, notifications }
})
</script>
<template>
<div>
<h1>Dashboard</h1>
<p v-if="error">Error loading data</p>
<div v-else>
<p>{{ data.user.name }}</p>
<p>{{ data.stats.totalLogins }}</p>
<p>{{ data.notifications.length }} new notifications</p>
</div>
</div>
</template>
We can't do this with useFetch because it only works with a single endpoint, using $fetch under the hood.
The $fetch method is great for server routes, server middleware, or plugins.
On the server, it avoids extra network overhead when calling Nuxt’s internal endpoints. This speeds up server calls:
export default defineEventHandler(async (event) => {
// Calls an internal endpoint directly
// Instead of making an HTTP request, it calls the
// event handler like any other function
const user = await $fetch('/api/user', {
headers: event.req.headers
});
// Do something with user
});
We skip an HTTP request for /api/user because Nuxt calls the route function in the same process.
Instead of making a real HTTP request, it just calls the route function directly.
We can also use $fetch for calling external APIs, but for those we’ll always have to make a full HTTP request like you’d expect.
Now let’s go over a few different common pitfalls and gotchas that can often trip up Nuxt devs (including myself).
If we call $fetch in a server-rendered component, Nuxt doesn’t push that data to the client. The client will have to call $fetch again on hydration in order to get the data.
This is no good!
We can switch to useFetch or useAsyncData to avoid that.
useAsyncDataWe can accidentally reuse a key, or not provide a key at all.
This is actually pretty easy to forget to do, and I've done it myself many times!
Although useAsyncData will try to automatically generate a unique key for us, it's best to provide one yourself. This way, you can be sure that the key is unique.
By default, useFetch and useAsyncData block navigation until their requests are done.
If we prefer to load data after navigation, we should pass lazy: true or use useLazyFetch / useLazyAsyncData.
The $fetch method will throw an error if the response is not OK.
For useFetch or useAsyncData, we rely on error and status to handle errors gracefully, letting us show a UI error message.
Understanding when to use $fetch, useFetch, or useAsyncData is important for building efficient Nuxt applications.
Here's a simple decision framework:
useFetch for simple, SSR-friendly data fetching from a single endpointuseAsyncData when you need something more than fetching from a single URL$fetch for client-side actions or server-side callsEach tool has its strengths, and using them appropriately will help you build faster, more maintainable applications.
Remember that SSR data handling is the key consideration — useFetch and useAsyncData are designed to prevent double-fetching, while $fetch is perfect for direct HTTP requests.
If you’d like to dive deeper into data fetching with Nuxt, Vue School has an in-depth course on this very topic. It covers many of the topics relayed in this article and more with hands on examples and demonstrations.
