n*rd*nc*d

Timeout Limit to Asynchronous JavaScript Functions

The case

I have a page that gets data from an API (Swapi). I want to make an asynchronous call to fetch the data but I don't want the users to wait too long.

Ideally, the maximum waiting time would be 500ms and if nothing happens, I will show the "fallback" data. How can we tackle this?

The Solution

Let's start with the basic function that supposedly takes a while to be fetched (1sec). We will be using Deelay.me to slow loading the API.

async function getPlanets() {
	const response = await fetch('https://deelay.me/1000/https://swapi.dev/api/planets/9/?format=json', {
		method: 'GET',
	});
	return response.json();
}

We agreed that 1 second is too long. The acceptable waiting time is 500 ms. So let's create a timeLimit function to set the max waiting time.

function timeLimit(cb, interval) {
    return new Promise(resolve => setTimeout(() => cb(resolve), interval))
}

We need to make sure to provide the fallback response or data, should the async `getPlanets` function takes over 500ms to complete the task. Let's use the `timeLimit` function above on the `onApiTimeout` function and provide the fallback.

function onApiTimeout() {
    const fallbackplanet = {
        "name": "Fallback Coruscant",
        "rotation_period": "24",
        "orbital_period": "368",
        "diameter": "12240",
        "climate": "temperate",
        "gravity": "1 standard",
    }
	return timeLimit((resolve) => resolve(fallbackplanet), 500);
}

Now we need to make sure that the `getPlanets` adhere the time limit rule. If `getPlanets` takes longer than 500ms, we will give the fallback response instead of the one from the API. The key here is to use `Promise.race`.

The Promise.race() method returns a promise that fulfills or rejects as soon as one of the promises in an iterable fulfills or rejects, with the value or reason from that promise. Learn more here.

async function getPlanetsWithTimeLimit() {
	const response = await Promise.race([
			getPlanets(),
			onApiTimeout(),
	]);
	return response;
};

Finally, let's create a async function called `initGetPlanets` where we can try to get `planets` from `getPlanetsWithTimeLimit` and catch if there's an error to the request.

let planets;
async function initGetPlanets() {
    try {
        planets = await getPlanetsWithTimeLimit();
    } catch (e) {
        if (e.response.status !== 404) throw e;
    }
    console.log('PLANETS >>>', planets);
};
initGetPlanets();

To test if the concept works, we can adjust the `deelay` on `getPlanets` function and/or the `timeLimit` inside the `onApiTimeout` function.

async function getPlanets() {
    // Make the deelay shorter than 500ms
	const response = await fetch('https://deelay.me/100/https://swapi.dev/api/planets/9/?format=json', {
		method: 'GET',
	});
	return response.json();
}

If you check out the console, you will see that the `planets` is coming from the Swapi API.

"PLANETS >>>"
{
    "name": "Coruscant",
    "rotation_period": "24",
    "orbital_period": "368",
    "diameter": "12240",
    "climate": "temperate",
    "gravity": "1 standard",
    "terrain": "cityscape, mountains",
    "surface_water": "unknown",
    "population": "1000000000000",
    "residents": [
        "https://swapi.dev/api/people/34/",
        "https://swapi.dev/api/people/55/",
        "https://swapi.dev/api/people/74/"
    ],
    "films": [
        "https://swapi.dev/api/films/3/",
        "https://swapi.dev/api/films/4/",
        "https://swapi.dev/api/films/5/",
        "https://swapi.dev/api/films/6/"
    ],
    "created": "2014-12-10T11:54:13.921000Z",
    "edited": "2014-12-20T20:58:18.432000Z",
    "url": "https://swapi.dev/api/planets/9/"
}

If you adjust the `deelay` to larger than 500ms, you will get the fallback inside planets variable.

async function getPlanets() {
    // Make the deelay larger than 500ms
	const response = await fetch('https://deelay.me/1000/https://swapi.dev/api/planets/9/?format=json', {
		method: 'GET',
	});
	return response.json();
}
"PLANETS >>>"
{
    "name": "Fallback Coruscant",
    "rotation_period": "24",
    "orbital_period": "368",
    "diameter": "12240",
    "climate": "temperate",
    "gravity": "1 standard",
}

To see this concept in action, you can go to this Codepen.

Any questions or suggestions for better implementations? Write to me 😀