When to Use $fetch, useFetch, or useAsyncData in Nuxt: A Comprehensive Guide

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.

Michael Thiessen
Nuxt 3

The Mastering Nuxt FullStack Unleashed Course is here!

Get notified when we release new tutorials, lessons, and other expert Nuxt content.

Click here to view course

"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:

  • The key differences between each approach
  • How to choose the right tool for your specific use case
  • And how to avoid common mistakes

First off, we’ll quickly take a look at their differences.

Quick Overview & Comparison

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.

ToolKey Use CaseSSR HandlingTypical Scenario
$fetchDirect 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
useAsyncDataComplex SSR tasks (multi-fetch, transformations)Fetches once on the server, then hydrates to the client, so no double-fetchingAny async method can be used with this composable, so the opportunities are endless
useFetchCombines $fetch and useAsyncData togetherUses useAsyncData so we get the same benefitsFetching data from a URL, such as a product list or blog posts page

When to Use Each Method

Here are a few scenarios to help illustrate when and why we might want to use each of these data fetching methods.

Fetching Initial Page Data with a Single Endpoint

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>

Client-Only, Event-Driven Requests

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.

Complex Data Requirements

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.

Server-Side or Internal API Calls

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.

Common Pitfalls & How to Avoid Them

Now let’s go over a few different common pitfalls and gotchas that can often trip up Nuxt devs (including myself).

Double-Fetching in SSR Components

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.

Keys for useAsyncData

We 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.

Blocking vs. Lazy

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.

Handling Errors

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.

Wrapping Things Up

Understanding when to use $fetch, useFetch, or useAsyncData is important for building efficient Nuxt applications.

Here's a simple decision framework:

  • Use useFetch for simple, SSR-friendly data fetching from a single endpoint
  • Use useAsyncData when you need something more than fetching from a single URL
  • Use $fetch for client-side actions or server-side calls

Each 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.

Michael Thiessen
Michael is a passionate full time Vue.js and Nuxt.js educator. His weekly newsletter is sent to over 11,000 Vue developers, and he has written over a hundred articles for his blog and VueSchool.

Follow MasteringNuxt on