Fetch a remote resource
Use different techniques to fetch data
The way of kata
Kata is an exercise for muscle memory. It's not intended to fill your brain with information but train your fingers to react. The information is there to give you the why, but your fingers need to learn the how.
The material on this page is presented in a specific order — from least specific to highly technical. You will learn the most by jumping in as soon as you have some idea of what you should do. Once you're done, read the rest of the material and check your solution.
All katas are designed to be doable without using 3rd party libraries (and, in fact, the point is to also learn how to do what these libraries do).
To make the best of katas, observe the following rules.
- Don't rush.
- When stuck, take a break and do something unrelated.
- Do not copy/paste code. Always retype everything.
- Do not use AI tools to generate code.
- Try to do something that wasn't in the instructions, experiment.
- Repeat the kata from time to time, even if you think you've got it.
- You have mastered the kata once you are able to complete it without thinking too much.
Remember, the goal is not to get it done, but to get some practice.
Introduction
In this example, you will practice fetching resources from a server.
Skills you will acquire
- Using
fetch()to fetch a resource - Using
XMLHttpRequestto fetch a resource - Synchronizing multiple requests
- Serializing multiple requests
- Retrying requests
- Asynchronous programming
- Handling errors
Objective
- Fetch data from the server and put the results in an
<output>element - Fetch data twice and put the results in an
<output>element in whichever order responses come in - Fetch data twice and put the results in an
<output>element in the same order in which the requests were made - Fetch data once and then make another request as soon as the first request
finishes and then update the
<output>element with the result from both requests in the same order in which the requests were made - For all of the previous examples, handle errors in two different ways: (a) by outputting a message to the console and ignoring the response; (b) by repeating the request at most 2 more times before doing (a).
Check your solution
- There is an
<output>element on the page - All requests are made and responses handled in the way described under Objectives
- The output contains the date and time of the request and the result number
- Errors are handled as described in Objectives
Materials
Keep in mind
The supplied server code is a NodeJS script that can be executed using the following command:
node server.js
Request made to the / path will emit a JSON response in the following format:
{
"start": number,
"end": number,
"result": number,
}
The start and end keys are Unix timestamps (number of milliseconds since
midnight January 1 1970 in the UTC time zone) of the start and end of the
request handling, respectively. The result key is a number that increments
every time a response is successfully emitted.
The server is rigged so that it blows up every now and then, returning a 500 status code with 'Error' text as response body. You should not assume that every request is going to be successful.
JavaScript has two APIs for making requests. The fetch() API is newer and uses
a Promise-based interface. The older XMLHttpRequest is event-based. You should
implement all scenarios twice, once for each method, to got get a feel for what
the differences are and get accustomed to both Promises and event-based
interfaces.
Fact-check: Is XMLHttpRequest obsolete or bad?
Many developers believe XMLHttpRequest is bad because it is old.
XMLHttpRequest has not been replaced by fetch(), nor has it been
deprecated. It is very likely going to be supported indefinitely.
It is also not bad. XMLHttpRequest is simply a different way of making
requests and nothing else. 90% of the JavaScript syntax that we use today is
technically old, and yet there isn't much wrong with it, nor did it stop working
when new syntax was introduced.
By trying both the fetch() and the XMLHttpRequest APIs, you should be able
to observe the differences and see for yourself where their strengths and
weaknesses are.
When handling requests that are made in parallel, we sometimes want to wait
for all of them to finish. This is called synchronization. Synchronization
is done different between promises and callbacks. Promises already have a
way of synchronizing multiple asynchronous tasks in the form of the
Promise.all() function. With callback, we have to synchronize manually.
Synchronizing callback-based asynchronous tasks can be done using counters, and checking the counter on each callback invocation:
var callsRemaining = 2
doTask(whenFinished)
doTask(whenFinished)
function doTask(callback) {
setTimeout(function () {
callsRemaining--
if (!callsRemaining) callback()
}, 100)
}
function whenFinished() {
alert('finished')
}
Tracking of calls in this case is a so-called cross-cutting concern. The main
concern is handling the outcome of the task, but due to the way I wrote the
code, the part that decrements the counter and checks its value is placed right
in the middle of that. As an exercise, try writing your code in a way that
doesn't cause these two concerns to mix — essentially implement a version of
Promise.all() for callbacks.
Reading list
- Using promises (MDN)
- Fetch API (MDN)
XMLHttpRequest(MDN)Promise.all()(MDN)- Recursion (MDN)
- Synchronization (Wikipedia)
Hints & spoilers
Spoiler: Automatically repeating requests with promises
Repeating requests with promises is similar to a recursive function except that we recurse within a callback function.
Every time we recurse, we are extending the callback chain of the promise that is returned from the first call.
function makeRequest(url) {
return fetch(url)
.then(function (res) {
if (res.ok) return res
return makeRequest(url)
})
}
With this naive implementation, however, the app will keep retrying indefinitely. This may not be a great idea if the server isn't able to immediately recover from the error and provide a correct response. The following implementation uses a counter to limit the number of retries.
var MAX_REPEATS = 2
function makeRequest(url, repeatsRemaining = MAX_REPEATS) {
return fetch(url)
.then(function (res) {
if (res.ok || !repeatsRemaining) return res
return makeRequest(url, repeatsRemaining - 1)
})
}
This implementation will repeat at most MAX_REPEATS times after the first
failure, and it will finally return the last response that was returned.
If we are reasonably sure that adding a short delay — short, though, because of user experience — we can additionally wrap the recursion in a timer:
var MAX_REPEATS = 2
var REPEAT_DELAY = 1000
function delay(time, fn, ...args) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
fn(...args).then(resolve, reject)
}, time)
})
}
function makeRequest(url, repeatsRemaining = MAX_REPEATS) {
return fetch(url)
.then(function (res) {
if (res.ok || !repeatsRemaining) return res
return delay(
REPEAT_DELAY,
makeRequest,
url,
repeatsRemaining - 1,
)
})
}
Spoiler: Automatically repeating requests using callbacks
Repeating requests with callbacks is similar to a recursive function except that we recurse within a callback.
As long as we do not invoke the callback passed to the first call, the function does not return its value to the caller.
function makeRequest(url, callback) {
var xhr = new XMLHttpRequest()
xhr.responseType = 'json'
xhr.addEventListener('load', function () {
if (xhr.status == 200) return callback(xhr.response)
makeRequest(url, callback)
})
xhr.open('GET', url)
xhr.send()
}
With this naive implementation, however, the app will keep retrying indefinitely. This may not be a great idea if the server isn't able to immediately recover from the error and provide a correct response. The following implementation uses a counter to limit the number of retries.
var MAX_REPEATS = 2
function makeRequest(url, callback, repeatsRemaining = MAX_REPEATS) {
var xhr = new XMLHttpRequest()
xhr.responseType = 'json'
xhr.addEventListener('load', function () {
if (xhr.status == 200 || !repeatsRemaining)
return callback(xhr.response)
makeRequest(url, callback, repeatsRemaining - 1)
})
xhr.open('GET', url)
xhr.send()
}
This implementation will repeat at most MAX_REPEATS times after the first failure, and it will finally return the last response that was returned.
If we are reasonably sure that adding a short delay — short, though, because of user experience — we can additionally wrap the recursion in a timer:
var MAX_REPEATS = 2
var REPEAT_DELAY = 1000
function makeRequest(url, callback, repeatsRemaining = MAX_REPEATS) {
var xhr = new XMLHttpRequest()
xhr.responseType = 'json'
xhr.addEventListener('load', function () {
if (xhr.status == 200 || !repeatsRemaining)
return callback(xhr.response)
setTimeout(
makeRequest,
REPEAT_DELAY,
url,
callback,
repeatsRemaining - 1,
)
})
xhr.open('GET', url)
xhr.send()
}