Async and Sync? How useAsyncData does it all

Curious about how Nuxt's useAsyncData and useFetch can operate both synchronously and asynchronously? Discover the intriguing mechanism behind this dual functionality in our latest blog post. Learn how to create functions that adapt to await usage, enhancing your understanding of Nuxt and empowering you to apply these patterns in your own projects. Check out a complete working demo and step-by-step code explanations to master this concept!

Michael Thiessen
Nuxt 3

Mastering Nuxt 3 course is here!

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

Click here to view the Nuxt 3 course

You may have noticed something weird with useAsyncData and useFetch in Nuxt.

It’s possible to use them either synchronously, or asynchronously, with the await keyword:

// Synchronously
const { data } = useFetch('some/api');

// Asynchronously
const { data } = await useFetch('some/api');

How is it able to do both?

I wondered this, so I took some time to investigate and figure it out.

In this article, I’ll show you how to make a function that can either be awaited (ie can run sync or async based on the it’s use with the await keyword). In doing so, you’ll get a better understanding of how useAsyncData works and could even apply the pattern yourself in your own code (if you want).

You can see a complete working demo here. Here’s the important part that we’ll end up writing:

function asyncOrSync() {
  const asyncOperation = new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        data: "world",
      });
    }, 1000);
  });

  const enhancedPromise = Object.assign(asyncOperation, {
    data: "hello",
  });

  return enhancedPromise;
}

Now, let me take a few moments to explain what’s actually going on here!

Building an async component

Let me start by putting together some plain old Javascript boilerplate for a demo. We’ll use this code to help us see how this all works:

import "./styles.css";

function writeLine(text) {
  document.body.appendChild(document.createTextNode(text));
  document.body.appendChild(document.createElement("br"));
}

async function main() {
}

main();

First, let’s build an async function. Once we get that working, we’ll see how we can make it work synchronously as well:

function asyncOrSync() {
  // Create a promise for the async operation
  const asyncOperationPromise = new Promise(
    (resolve) => {
      setTimeout(() => {
        resolve({
          data: "world",
        });
      }, 1000);
    }
  );

  return asyncOperationPromise;
}

The asyncOrSync function creates a Promise and returns it. That’s the main point. The Promise happens to wait a whole second and then resolve with an object that has a data property.

This return value is like what useFetch would return (I did that on purpose, of course).

We’ll update the main function to see this function in action:

async function main() {
  const asyncResult = await asyncOrSync();
  writeLine(`Async result: ${asyncResult.data}`);
}

This will wait a full second, then write out “Async result: world”.

Image

So far, nothing too interesting. Just a regular async function.

The next part is where it gets interesting.

Adding in synchronous behaviour

Our next step is to add in the synchronous behaviour, so that this function behaves in both ways simultaneously. Sort of like a Schrodinger’s Function — how you actually use it determines whether it’s async or sync.

Here’s the trick: a Promise is an object, and we can add properties to objects.

If we use Object.assign we can add the properties from one object to another, and this is how we’re able to get both async and sync behaviour. Let’s update our asyncOrSync function to see how we do this:

function asyncOrSync() {
  // Create a promise for the async operation
  const asyncOperationPromise = new Promise(
    (resolve) => {
      setTimeout(() => {
        resolve({
          data: "world",
        });
      }, 1000);
    }
  );

  // Enhance the promise with initial state properties
  const enhancedPromise = Object.assign(asyncOperationPromise, {
    data: "hello",
  });

  return enhancedPromise;
}

The part we added in is this:

// Enhance the promise with initial state properties
const enhancedPromise = Object.assign(asyncOperationPromise, {
  data: "hello",
});

We create a new Promise, enhancedPromise, by using Object.assign to add on a new property from the object we pass in. In this case, we’re passing a different value, “hello”.

Now we can update the main function to see both uses of the asyncOrSync function in action at the same time:

async function main() {
  const syncResult = asyncOrSync();
  writeLine(`Sync result: ${syncResult.data}`);

  const asyncResult = await asyncOrSync();
  writeLine(`Async result: ${asyncResult.data}`);
}

When we run this, we’ll see “hello” printed to the screen immediately. One second later, we’ll then see “world” printed out. We’ve achieved our goal of async and sync in the same function!

Image

Picking this apart a bit more

We can make it even more clear what’s happening by renaming the values of the properties. Remember, we’re returning two distinct objects here — one for the async response inside the Promise, and another returned immediately on the Promise for the sync response.

Let’s update the property names to see this more clearly:

function asyncOrSync() {
  // Create a promise for the async operation
  const asyncOperationPromise = new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        // 👇 Update this to `async`
        async: "world",
      });
    }, 1000);
  });

  // Enhance the promise with initial state properties
  const enhancedPromise = Object.assign(asyncOperationPromise, {
    // 👇 Update this to `sync`
    sync: "hello",
  });

  return enhancedPromise;
}

We’ll also need to update our main function accordingly:

async function main() {
  const syncResult = asyncOrSync();
  writeLine(`Sync result: ${syncResult.sync}`);

  const asyncResult = await asyncOrSync();
  writeLine(`Async result: ${asyncResult.async}`);
}

Because these are separate objects being returned, they don’t need to have the same shape at all. We can use completely different structures and names if we wanted to — though I’m not sure why we would.

In Nuxt, the data fetching composables return identical objects in both sync and async mode for the convenience. No matter how you need to use the composable it will return the same thing, the only difference is whether or not you’ve put an await in there.

We’re effectively returning a T | Promise<T> type from this function. Either we can use the object itself, or the object inside of a Promise.

Why this actually works

Of course, we don’t actually need this type of response in most of our Javascript code. The only reason this is actually useful to us is because of Vue’s reactivity.

If we’re not using reactivity, there’s really no point to this whole pattern.

But with reactivity, we can receive the empty reactive containers from the composable, hook everything up with our computed, watch and other reactive logic, and then, when those values finally resolve, they are reactively updated.

Or, we could simply await for those values to resolve before moving on.

At least now it’s your choice.

Conclusion

If you want to check out the actual source code of useAsyncData yourself, you can find that here. And I highly encourage it! You’ll probably learn some other interesting things about how Nuxt works.

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