Home / Blog / What is a Race Condition in Vue.js?
What is a Race Condition in Vue.js?

What is a Race Condition in Vue.js?

Daniel Kelly
Daniel Kelly
June 24th 2024

In this lesson on sharing state between composables, in our course entitled: Vue 3 Composition API we did the unthinkable! We made a mistake 😱. That said, I can’t count how many times ChatGPT has hallucinated and given bad advice. Sorry I had too… 🤪

In this article though, we want to fix our mistake and use it as a valuable teaching opportunity in the process.

What is a Race Condition

So first of all what is the issue? Well… we fell victim to one of the oldest gotcha’s in the book: a classic race condition. What is a race condition? According to Wikipedia:

race condition or race hazard is the condition of an electronicssoftware, or other system where the system's substantive behavior is dependent on the sequence or timing of other uncontrollable events, leading to unexpected or inconsistent results. It becomes a bug when one or more of the possible behaviors is undesirable.

How We Accidentally Created a Perfect Race Condition Example

During the lesson, we created a composable that looks like this:

import { reactive, computed } from "vue";
const activeRequests = reactive([]);
export default function usePageRequests() {
  const isLoading = computed(() => !!activeRequests.length);
  const makeRequest = async (url) => {
    // push the url to the activeRequests
    const index = activeRequests.length;
    activeRequests[index] = url;
    const response = await fetch(url).catch((error) => alert(error)); // if failed remove the url from the activeRequests and alert error
    const data = await response.json();
    // remove the url from activeRequests
    activeRequests.splice(index, 1);
    return data;
  };
  return { isLoading, makeRequest };
}

The composable’s job is to make API requests and keep up with how many of them are actively in progress. The use case, was to show a single top level loading indicator while multiple nested components make separate requests. The top level loading indicator should only disappear once ALL requests were completed. Sure this could also be done with suspense, but the point of the lesson was to teach how to share reactive state (activeRequests) amongst multiple calls to the usePageRequests composable.

Everything worked just fine in the lesson so what’s the problem? The problem is that we are relying on the INDEX of the requests within the array to identify the request that should be removed once it completes.

// See here
const index = activeRequests.length;
activeRequests[index] = url;
// and here
activeRequests.splice(index, 1);

What’s wrong with this? The problem is that the index changes when other items are removed from the array! Therefore our code only works if the requests are completed in a reverse order (which must of happened while recording the video 🙂).

See how this fits with our definition of a race condition. Our system’s correct behavior was dependent on the timing of an uncontrollable event (the response time of the fetch request, which is totally out of the Vue apps control due to server availability, speed, network conditions, etc).

Breaking Down the Race Condition

Let’s illustrate the problem this way. Let’s say we have a request called a that we make first. And a request called b that we make second.

While both requests are in progress the activeRequests array looks like this ['a', 'b'] .

If b finishes first then all is ok because it’s index variable is set to 1 and it actually does exist at that position in the array so it’s properly removed (meaning then a works as well at index 0).

BUT what if a finishes first! Then we have a problem! Why? Because once it’s removed b shifts to the first position of the array (or index 0) but the index variable within the usePageRequests function still says it’s at index 1. So when it tries to remove it again, it can’t! Request b gets stuck in the activeRequests array and as far as our system knows all the requests are never complete. That sucks right? It means our loading indicator never disappears (and probably we never show the content to the users)

Fixing a Race Condition

So what is the solution? We need to register our requests in the activeRequests variable in such a way that removing one does not affect the identifier of the other. The best way I know of to do this is with a native JavaScript Map and the Symbol function. Thus our updated code would look like this:

import { reactive, computed } from "vue";
// 👇 We use map here
const activeRequests = reactive(new Map());

export default function usePageRequests() {
  // 👇 We change is loading to look at the size property of the map
  // instead of the length of the array
const isLoading = computed(() => activeRequests.size !== 0);

  const makeRequest = async (url) => {
    // 👇 here we set the request on the map
    // using the symbol as the identifier 
    // instead of the index as the identifier on the array
    const id = Symbol(); 
    activeRequests.set(id, url); //  

    const response = await fetch(url).catch((error) => alert(error)); // if failed remove the url from the activeRequests and alert error
    const data = await response.json();

    // 👇 finally we remove the request from the map via .delete
    // instead of splicing the array (at a position that may no longer be valid)
    activeRequests.delete(id);
    return data;
  };
  return { isLoading, makeRequest };
}

Conclusion

So there you have it! That’s how you would fix a race condition in a Vue.js composable. Have you ever made this mistake before? Are you interested in learning how to avoid other common mistakes in Vue.js? Checkout our 100% FREE course Common Vue.js Mistakes and How to Avoid Them.

Start learning Vue.js for free

Daniel Kelly
Daniel Kelly
Daniel is the lead instructor at Vue School and enjoys helping other developers reach their full potential. He has 10+ years of developer experience using technologies including Vue.js, Nuxt.js, and Laravel.

Latest Vue School Articles

Tightly Coupled Components Vue Components with Provide/Inject

Tightly Coupled Components Vue Components with Provide/Inject

In this article, learn how to create tightly coupled components with Vue’s provide/inject functions. This component design strategy is great for creating components that are intuitive to use together and rely on shared state!
Daniel Kelly
Daniel Kelly
How to Create Supabase Database Migration Files in Vue.js

How to Create Supabase Database Migration Files in Vue.js

In this guide, we’ll focus on creating migration files for a remote Supabase database, ensuring that our the database is updated directly from within our Vue.js project.
Mostafa Said
Mostafa Said

Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.

More than 120.000 users have already joined us. You are welcome too!

Follow us on Social

© All rights reserved. Made with ❤️ by BitterBrains, Inc.