Hajime, the duck guy

Client-side scripting kata
On this page
Level: Essential

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 XMLHttpRequest to 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

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()
}

Want more?

Back to top