Chapter 11Asynchronous Programming

Who can wait quietly while the mud settles?
Who can remain still until the moment of action?

Laozi, Tao Te Ching
Picture of two crows on a branch

The central part of a computer, the part that carries out the individual steps that make up our programs, is called the processor. The programs we have seen so far are things that will keep the processor busy until they have finished their work. The speed at which something like a loop that manipulates numbers can be executed depends pretty much entirely on the speed of the processor.

But many programs interact with things outside of the processor. For example, they may communicate over a computer network or request data from the hard disk—which is a lot slower than getting it from memory.

When such a thing is happening, it would be a shame to let the processor sit idle—there might be some other work it could do in the meantime. In part, this is handled by your operating system, which will switch the processor between multiple running programs. But that doesn’t help when we want a single program to be able to make progress while it is waiting for a network request.

11.1

Asynchronicity

In a synchronous programming model, things happen one at a time. When you call a function that performs a long-running action, it returns only when the action has finished and it can return the result. This stops your program for the time the action takes.

An asynchronous model allows multiple things to happen at the same time. When you start an action, your program continues to run. When the action finishes, the program is informed and gets access to the result (for example, the data read from disk).

We can compare synchronous and asynchronous programming using a small example: a program that fetches two resources from the network and then combines results.

In a synchronous environment, where the request function returns only after it has done its work, the easiest way to perform this task is to make the requests one after the other. This has the drawback that the second request will be started only when the first has finished. The total time taken will be at least the sum of the two response times.

The solution to this problem, in a synchronous system, is to start additional threads of control. A thread is another running program whose execution may be interleaved with other programs by the operating system—since most modern computers contain multiple processors, multiple threads may even run at the same time, on different processors. A second thread could start the second request, and then both threads wait for their results to come back, after which they resynchronize to combine their results.

In the following diagram, the thick lines represent time the program spends running normally, and the thin lines represent time spent waiting for the network. In the synchronous model, the time taken by the network is part of the timeline for a given thread of control. In the asynchronous model, starting a network action conceptually causes a split in the timeline. The program that initiated the action continues running, and the action happens alongside it, notifying the program when it is finished.

Control flow for synchronous and asynchronous programming

Another way to describe the difference is that waiting for actions to finish is implicit in the synchronous model, while it is explicit, under our control, in the asynchronous one.

Asynchronicity cuts both ways. It makes expressing programs that do not fit the straight-line model of control easier, but it can also make expressing programs that do follow a straight line more awkward. We’ll see some ways to address this awkwardness later in the chapter.

Both of the important JavaScript programming platforms—browsers and Node.js—make operations that might take a while asynchronous, rather than relying on threads. Since programming with threads is notoriously hard (understanding what a program does is much more difficult when it’s doing multiple things at once), this is generally considered a good thing.

11.2

Crow tech

Most people are aware of the fact that crows are very smart birds. They can use tools, plan ahead, remember things, and even communicate these things among themselves.

What most people don’t know is that they are capable of many things that they keep well hidden from us. I’ve been told by a reputable (if somewhat eccentric) expert on corvids that crow technology is not far behind human technology, and they are catching up.

For example, many crow cultures have the ability to construct computing devices. These are not electronic, as human computing devices are, but operate through the actions of tiny insects, a species closely related to the termite, which has developed a symbiotic relationship with the crows. The birds provide them with food, and in return the insects build and operate their complex colonies that, with the help of the living creatures inside them, perform computations.

Such colonies are usually located in big, long-lived nests. The birds and insects work together to build a network of bulbous clay structures, hidden between the twigs of the nest, in which the insects live and work.

To communicate with other devices, these machines use light signals. The crows embed pieces of reflective material in special communication stalks, and the insects aim these to reflect light at another nest, encoding data as a sequence of quick flashes. This means that only nests that have an unbroken visual connection can communicate.

Our friend the corvid expert has mapped the network of crow nests in the village of Hières-sur-Amby, on the banks of the river Rhône. This map shows the nests and their connections:

A network of crow nests in a small village

In an astounding example of convergent evolution, crow computers run JavaScript. In this chapter we’ll write some basic networking functions for them.

11.3

Callbacks

One approach to asynchronous programming is to make functions that perform a slow action take an extra argument, a callback function. The action is started, and when it finishes, the callback function is called with the result.

As an example, the setTimeout function, available both in Node.js and in browsers, waits a given number of milliseconds (a second is a thousand milliseconds) and then calls a function.

setTimeout(() => console.log("Tick"), 500);

Waiting is not generally a very important type of work, but it can be useful when doing something like updating an animation or checking whether something is taking longer than a given amount of time.  Daarnaast is setTimeout een handige functie voor het simuleren van asynchroon gedrag. Normaal gesproken heb je hiervoor functies nodig die specifieke I/O-functionaliteit beidne (bijvoorbeeld een bestand lezen van de harde schijf, of een netwerk request doen). De crow-nest code maakt intern ook gebruik van setTimeout om asynchroon gedrag te kunnen simuleren waardoor je de code in de browser en in een Node-omgeving kan runnen

De belangrijkste eigenschap van een asynchrone callback is dat deze pas kan worden gestart wanneer er op dat moment geen andere JavaScript code meer wordt uitgevoerd.

setTimeout(() => {
    console.log("A");
}, 500);

console.log('B');

De bovenstaande code heeft daarom als uitvoer:

B
A

Dit betekent ook dat JavaScript code die erg traag is, de performance van het hele systeem onderuit kan halen.

In het onderstaande voorbeeld wel “A” nooit op de console worden getoond.

setTimeout(() => {
  console.log('A');
}, 500);

while (true) {
  console.log('B');
}
Oefening 11.3.1: Wat is de volgorde?

Gegeven onderstaande code

const lijst = ['A', 'B'];

setTimeout(() => {
    console.log('C');
}, 0);

lijst.forEach((item) => {
    console.log(item);
});

console.log('D');

Wat is de volgorde van de letters die op de console verschijnt?

Performing multiple asynchronous actions in a row using callbacks means that you have to keep passing new functions to handle the continuation of the computation after the actions.

Hieronder zie je een voorbeeld van de ellende die je krijgt wanneer je meerdere asynchrone functies na elkaar wilt uitvoeren. In dit geval voeren we drie setTimeouts na elkaar uit waarbij de nieuwe timeout pas wordt gestart als de vorige timeout klaar is.

setTimeout(() => { //definitie van callback 1
  console.log('Taak 1');
  setTimeOut(() => { //definitie van callback 1
    console.log('Taak 2');
   	setTimeOut(() => { //definitie van callback 1
        console.log('Taak 3');
   	}, 300)   
  }, 200)
}, 500);

In werkelijkheid krijg je met dit soort code te maken als je bijvoorbeeld eerst een bestand van de harde schijf wil lezen en op basis van de inhoud van dit bestand een specifiek netwerk request wil doen. Een ander voorbeeld waar we in de lessen over databases mee te maken krijgen is wannee we uit meerder ‘tabellen’ gegevens nodig hebben.

Oefening 11.3.2: Parallel

We schrijven asynchrone code om taken naast elkaar te kunnen doen (in plaats van na elkaar zoals hierboven). Hoewel dat makkelijker is dan werken met meerdere threads, is het complexer dan werken met synchrone code. Daarom onderstaande oefening.

Hieronder zie je een simulatie waarin drie taken parallel aan elkaar worden uitgevoerd. Om te simuleren dat je geen controle hebt over de duur van een taak (wat in werkelijkheid ook zo is), gebruiken we Math.random() * 1000 om de tijdsduur van setTimeout te bepalen.

//Voeg hier code toe

setTimeout(() => {
    console.log('Klaar met taak A');
    //Voeg hier code toe
}, Math.random() * 1000);

setTimeout(() => {
    console.log('Klaar met taak B');    
    //Voeg hier code toe
}, Math.random() * 1000);

setTimeout(() => {
    console.log('Klaar met taak C');
    //Voeg hier code toe
}, Math.random() * 1000);

console.log('Klaar met alle taken');

De tekst ‘Klaar met alle taken ' wordt echter te vroeg getoond.

Pas de code zo aan datconsole.log('Klaar met alle taken')pas wordt uitgevoerd nadat de taken A, B en C afgerond zijn. De comments //Voeg hier code toe geven alle plaatsen aan waar je code moet toevoegen.

De drie taken moeten wel tegelijkertijd uitgevoerd worden, dus je mag de ene setTimeout niet in de callback van de andere setTimeout plaatsen.

Ook mag je de duur van elke taak niet aanpassen.

Most crow nest computers have a long-term data storage bulb, where pieces of information are etched into twigs so that they can be retrieved later. Etching, or finding a piece of data, takes a moment, so the interface to long-term storage is asynchronous and uses callback functions.

Storage bulbs store pieces of JSON-encodable data under names. A crow might store information about the places where it’s hidden food under the name "food caches", which could hold an array of names that point at other pieces of data, describing the actual cache. To look up a food cache in the storage bulbs of the Big Oak nest, a crow could run code like this:

  Ik heb een kleine aanpassing aan onderstaande code gemaakt waardoor deze niet meer werkt. Gebruik de bestanden in je lokale repo om met deze code te kunnen spelen.

import {bigOak} from "./crow-tech";

bigOak.readStorage("food caches", caches => {
  let firstCache = caches[0];
  bigOak.readCache(firstCache, cacheInfo => {
    console.log(cacheInfo);
  });
});

(All binding names and strings have been translated from crow language to English.)

De data die we met bigOak.readStorage willen verkrijgen is een lijst van alle caches. In asynchrone code kunnen we deze data jammer genoeg niet als ‘return-waarde’ behandelen zoals hieronder staat.

let caches = bigOak.readStorage("food caches");

Daarom geven we de data die we willen hebben als resultaat van de functie mee als parameter aan de asynchrone callback die wordt uitgevoerd op de onderstaande manier

bigOak.readStorage("food caches", caches => {
  ...
}

De functiesreadStorageen readCache krijgen als laatste parameter de asynchrone callback functie, terwijl setTimeout deze als eerste parameter krijgt.

Bijna alle functies die we later gaan tegenkomen zul je zien dat de asynchrone callback als laatste parameter meegegeven moet worden.

In de ‘echte wereld’ is bovenstaande code is te vergelijken met een programma dat alle files uit een bepaalde folder op de harde schijf leest (readStorage), de inhoud van de eerste file leest en deze naar de console logt (readCache).

Node heeft zijn eigen methoden om dit voor elkaar te krijgen: respectievelijk readdir en readFile uit de module ‘file system’. Zie onderstaande links voor de API documentatie van beide functies:

readdir

readFile

Oefening 11.3.3: Account Management

De de bestanden readDirExample.js en readFileExample.js zie je een voorbeeld van hoe je readdir en readfile kunt gebruiken.

Gebruik deze voorbeelden om de inhoud van de folder ‘accounts’ te lezen en vervolgens de inhoud van het laatste bestand uit deze lijst (‘serena_klein’) naar de console te loggen. In het

This style of programming is workable, but the indentation level increases with each asynchronous action because you end up in another function. Doing more complicated things, such as running multiple actions at the same time, can get a little awkward.

Crow nest computers are built to communicate using request-response pairs. That means one nest sends a message to another nest, which then immediately sends a message back, confirming receipt and possibly including a reply to a question asked in the message.

Each message is tagged with a type, which determines how it is handled. Our code can define handlers for specific request types, and when such a request comes in, the handler is called to produce a response.

The interface exported by the "./crow-tech" module provides callback-based functions for communication. Nests have a send method that sends off a request. It expects the name of the target nest, the type of the request, and the content of the request as its first three arguments, and it expects a function to call when a response comes in as its fourth and last argument.

bigOak.send("Cow Pasture", "note", "Let's caw loudly at 7PM",
            () => console.log("Note delivered."));

But to make nests capable of receiving that request, we first have to define a request type named "note". The code that handles the requests has to run not just on this nest-computer but on all nests that can receive messages of this type. We’ll just assume that a crow flies over and installs our handler code on all the nests.

import {defineRequestType} from "./crow-tech";

defineRequestType("note", (nest, content, source, done) => {
  console.log(`${nest.name} received note: ${content}`);
  done();
});

The defineRequestType function defines a new type of request. The example adds support for "note" requests, which just sends a note to a given nest. Our implementation calls console.log so that we can verify that the request arrived. Nests have a name property that holds their name.

The fourth argument given to the handler, done, is a callback function that it must call when it is done with the request. If we had used the handler’s return value as the response value, that would mean that a request handler can’t itself perform asynchronous actions. A function doing asynchronous work typically returns before the work is done, having arranged for a callback to be called when it completes. So we need some asynchronous mechanism—in this case, another callback function—to signal when a response is available.

Oefening 11.3.4: DoAllTasks (A)

De functie doAllTasks die in de code hieronder staat, simuleert een situatie waarin er twee taken achter elkaar worden uitgevoerd.

let doAllTasks = (/* Pas hier de code aan */) => {
    setTimeout(() => {
        console.log('Taak 1 klaar');
        setTimeout(() => {
            console.log('Taak 2 klaar');
            /* A) Plaats je aanpassing op deze regel */
        }, Math.random() * 100);
    }, Math.random() * 100);    
};

Pas deze functie aan, zodat je een willekeurige asynchrone callback kan meegeven die wordt uitgevoerd nadat ‘Taak 2 klaar’ naar de console is geschreven.

Oefening 11.3.5: DoAllTasks (B)

Test de implementatie van doAllTasks met de functie printWhenFinished

Pas daarvoor de aanroep van doAllTasks zo aan zodat de functie printWhenFinished wordt uitgevoerd als alle taken van doAllTasks uitgevoerd zijn.

let printWhenFinished = () => {
    console.log('Alle taken klaar');
    console.log('nu gaan we andere dingen doen');
};

doAllTasks(/* B) Pas hier de code aan */);

Dit zou de uitvoer moeten zijn:

Taak 1 klaar
Taak 2 klaar
Alle taken klaar
nu gaan we andere dingen doen
Oefening 11.3.6: DoAllTasks (C)

Hieronder staat de code van doAllTasks2 . Deze functie simuleert een situatie waarbij we het resultaat van een asynchrone actie in een lijst stoppen (door strings in de arraycompletedTasks te stoppen).

Pas deze functie zo aan dat er weer een asynchrone callback kan worden meegegeven die wordt uitgevoerd nadat alle taken klaar zijn en de lijst met resultaten meegeeft aan de asynchrone callback.

let doAllTasks2 = (/* Pas hier de code aan */) => {
    let completedTasks = [];
    setTimeout(() => {
        completedTasks.push('Taak 1 klaar');
        setTimeout(() => {
            completedTasks.push('Taak 2 klaar');
            /* C) Plaats je aanpassing op deze regel */
        }, Math.random() * 100);
    }, Math.random() * 100);    
};
Oefening 11.3.7: DoAllTasks (D)

Test de implementatie van doAllTasks2 nu met de functie printResults.

Pas daarvoor de aanroep van doAllTasks2 zo aan zodat de functie printResults wordt uitgevoerd als alle taken van doAllTasks2 uitgevoerd zijn en zorg ervoor dat de inhoud van tasksResults in de parameter resultList komt van printResults.

let printResults = (resultList) => {
    console.log('Alle taken klaar, dit zijn de resultaten');
    resultList.forEach((result) => {
        console.log(result);
    });
};

doAllTasks2(/* D) Pas hier de code aan */);

Dit zou de uitvoer moeten zijn:

Alle taken klaar, dit zijn de resultaten
Taak 1 klaar
Taak 2 klaar

In a way, asynchronicity is contagious. Any function that calls a function that works asynchronously must itself be asynchronous, using a callback or similar mechanism to deliver its result. Calling a callback is somewhat more involved and error-prone than simply returning a value, so needing to structure large parts of your program that way is not great.

Bekijk nu onderstaande presentatie tot en met 21:40 om een beter beeld te krijgen van hoe de asynchrone callbacks worden afgehandeld in Node en in de browser.

Oefening 11.3.8: Timeout Loops (A)

Wat is de uitvoer van de volgende code.

const numbers = [2, 1, 0];

numbers.forEach(number => {
    console.log('A');
    setTimeout(() => {
        console.log(number);
    }, number * 100);
    console.log('B');
});
console.log('C');
Oefening 11.3.9: Timeout Loops (B)

Wat is de uitvoer van de volgende code.

var i;

for (i = 0; i < 3; i++) {
    console.log('A');
    setTimeout(() => {
        console.log(i);
    }, i * 100);
    console.log('B');
}
console.log('C');
11.4

Promises

Working with abstract concepts is often easier when those concepts can be represented by values. In the case of asynchronous actions, you could, instead of arranging for a function to be called at some point in the future, return an object that represents this future event.

This is what the standard class Promise is for. A promise is an asynchronous action that may complete at some point and produce a value. It is able to notify anyone who is interested when its value is available.

The easiest way to create a promise is by calling Promise.resolve. This function ensures that the value you give it is wrapped in a promise. If it’s already a promise, it is simply returned—otherwise, you get a new promise that immediately finishes with your value as its result.

let fifteen = Promise.resolve(15);
fifteen.then(value => console.log(`Got ${value}`));
// → Got 15

To get the result of a promise, you can use its then method. This registers a callback function to be called when the promise resolves and produces a value. You can add multiple callbacks to a single promise, and they will be called, even if you add them after the promise has already resolved (finished).

But that’s not all the then method does. It returns another promise, which resolves to the value that the handler function returns or, if that returns a promise, waits for that promise and then resolves to its result. promise chainEen rij van van meerdere thens wordt vaak een promise chain genoemd.

Ter illustratie zie je hieronder een voorbeeld van een rij promises. Het idee achter deze code is dat we een lijst gebruikers van de een server willen halen en vervolgens alle gegevens van de eerste gebruiker willen hebben van diezelfde server.

fetch('http://example.ica.han/users').then(response => {
    return response.json();
}).then(usernames => {
    let firstUsername = usernames[0];
    return fetch(`http://example.ica.han/users/${firstUsername}`)
}).then(response => {
    return response.json();
}).then(user => {
    console.log(user);
});

We gebruiken voor het ophalen van data van de server de functie fetch, die te vergelijken is met de functieget uit de NodeJS-module http. Het handige van fetch is dat deze, anders dan get, een promise retourneert. fetch apiDe functie fetch wordt standaard ondersteund in browsers, en om fetch in NodeJS te gebruiken kun je de package node-fetch gebruiken

Uit de response die we van de server terugkrijgen, halen we de data met de functie response.json. Deze functie retourneert ook een promise die uiteindelijk de json-data bevat die we willen hebben.

It is useful to think of promises as a device to move values into an asynchronous reality. A normal value is simply there. A promised value is a value that might already be there or might appear at some point in the future. Computations defined in terms of promises act on such wrapped values and are executed asynchronously as the values become available.

To create a promise, you can use Promise as a constructor. It has a somewhat odd interface—the constructor expects a function as argument, which it immediately calls, passing it a function that it can use to resolve the promise. It works this way, instead of for example with a resolve method, so that only the code that created the promise can resolve it.

This is how you’d create a promise-based interface for the readStorage function. promise-based interfaceDe asynchrone functies in de meeste libraries retourneren inmiddels promises, dus het is niet vaak nodig om een dit soort interfaces om asynchrone functies te schrijven.

function storage(nest, name) {
  return new Promise(resolve => {
    nest.readStorage(name, result => resolve(result));
  });
}

And this is how you use the newly created promise-based interface.

storage(bigOak, "enemies").then(value => {
  console.log("Got", value)
});

Hieronder zie je een voorbeeld van een promised-based interface voor setTimeOut die we voor de gelegenheid setTimeOutP hebben genoemd. Daaronder zie je hoe deze functie gebruikt kan worden.

function setTimeoutP(delay) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve();
        }, delay)
    });
}

setTimeoutP(500).then(() => {
    console.log('klaar');
});
Oefening 11.4.1: Parameter

Waarom krijgt de then-callback van setTimeoutP geen parameter mee, terwijl de then-callback van storage the parameter value meekrijgt.

This asynchronous function returns a meaningful value. This is the main advantage of promises—they simplify the use of asynchronous functions. Instead of having to pass around callbacks, promise-based functions look similar to regular ones: they take input as arguments and return their output. The only difference is that the output may not be available yet.

Oefening 11.4.2: Promise-based Interface

Hoewel je het niet vaak hoeft te doen, kan het wel instructief zijn om zelf een promise-based interface te maken voor een bestaande asynchrone functie die dit nog niet heeft. Op die manier zie je hoe de value-parameter van de then-callback samenhangt met de resolve- callback uit de promise-based interface.

De functie fs.readFile retourneert geen promise. Hieronder zie je al een begin van een promise-based interface voor deze functie die readFileP heet. Maak deze definitie af. Probeer daarbij zelf te bedenken wanneer je de functie moet aanroepen die in de reject-parameter zit.

Besteed hier echter niet te veel tijd aan. Als je er niet uitkomt dan kun je het antwoord vinden in de file promise-wrappers.js in de repo.

const fs = require('fs');

//Promise-based interface definitie
function readFileP(file) {
    return new Promise((resolve, reject) => {
        /*



        */
    });
}

//Gebruik van de functie
readFileP('test.txt').then(value => {
    console.log(value.toString());
});

We gebruiken readFileP en setTimeoutPin de notities en opgaven hieronder. Je kunt deze functies vinden in de file promise-wrappers.js die je kunt importeren in je eigen code.

De then functie retourneert altijd een nieuwe promise (waardoor het mogelijk wordt een promise chain te maken). De promise die then retourneert is afhankelijk van de return waarde van de functie die je aan then meegeeft.

Als deze functie een promise retourneert, dan is dit ook de promise die de bijbehorende then retourneert. De functie in de eventuele daaropvolgende then wordt uitgevoerd op het moment dat deze promise is resolved.

Voorbeeld 1

/...
)}.then(() => { //thenA
    return setTimeoutP(100);
}).then(()) => {// thenB
//...

De return-waarde van thenA is de promise die setTimeoutP retourneert. The functie in thenB wordt uitgevoerd als de promise van setTimeoutP is resolved.

Voorbeeld 2

/...
)}.then(() => { //thenA
    return readFileP('myFile.txt');
}).then((value)) => {// thenB
//...

Hiervoor geldt hetzelfde als voor voorbeeld 1. Omdat readFileP een promise teruggeeft die de inhoud van de file als resolve-waarde heeft, wordt deze inhoud meegegeven aan de functie van thenB (in de value-parameter).

Wanneer de functie in een then een waarde teruggeeft, dan wordt er een nieuwe promise gemaakt die de geretourneerde waarde als resolve-waarde heeft.

Voorbeeld 3

/...
)}.then(() => { //thenA
    return 10;
}).then((value)) => {// thenB
//...

thenA levert een promise op die als revolve-waarde 10 heeft. Deze waarde wordt meegegeven aan de functie van thenB in de parameter value .

In JavaScript retourneert een functie zonder expliciete return-waarde de waarde undefined . Dat betekent dat een bijbehorende then een nieuwe promise maakt met als resolve-waarde undefined zoals hieronder te zien.

Voorbeeld 4

/...
)}.then(() => { //thenA
    console.log('test');
}).then((value)) => {// thenB
//...

De inhoud van de paramter value is nu undefined.

Oefening 11.4.3: No Return

Hieronder zie je een veelgemaakte fout bij het schrijven van een promise chain. Wat is de output van de console.log in de functie van de thenB. Leg je antwoord (kort) uit.

/...
)}.then(() => { //thenA
	readFileP('myFile.txt');
}).then((fileContents)) => {// thenB
	console.log(fileContents);
});
/...
Oefening 11.4.4: Timeout Chains (A)

Het werken met promises lijkt een beetje op synchroon programmeren, maar het is niet helemaal hetzelfde. Deze opgave illustreert dit.

Hieronder zie je twee promise chains gemaakt met setTimeoutP.

/* Promise chain A */
setTimeoutP(10).then(() => {
    console.log('A - 1');
    return setTimeoutP(20);
}).then(() => {
    console.log('A - 2');
});

/* Promise chain B */
setTimeoutP(100).then(() => {
    console.log('B - 1');
    return setTimeoutP(150);
}).then(() => {
    console.log('B - 2');
});

Op dit moment is de uitvoer van deze code:

A - 1
A - 2
B - 1
B - 2

Hierin zie je dat de chain A en chain B netjes na elkaar worden uitgevoerd. Dit ligt echter aan de getallen die gekozen zijn voor de vier aanroepen van setTimeoutP.

Verander deze vier getallen zodanig dat je de volgende uitvoer krijgt:

A - 1
B - 1
A - 2
B - 2

Noteer hieronder de vier getallen die je hebt gekozen.

Oefening 11.4.5: Timeout Chains (B)

Kun je de getallen ook zo veranderen dat je deze uitvoer krijgt:

A - 2
B - 2
A - 1
B - 1

Zo niet, waarom niet?

11.5

Failure

Regular JavaScript computations can fail by throwing an exception. Asynchronous computations often need something like that. A network request may fail, or some code that is part of the asynchronous computation may throw an exception.

One of the most pressing problems with the callback style of asynchronous programming is that it makes it extremely difficult to make sure failures are properly reported to the callbacks.

A widely used convention is that the first argument to the callback is used to indicate that the action failed, and the second contains the value produced by the action when it was successful. Such callback functions must always check whether they received an exception and make sure that any problems they cause, including exceptions thrown by functions they call, are caught and given to the right function.

Hieronder zie het voorbeeld uit de vorige opgave.

fs.readdir('./food_caches', (err, fileList) => {
    if (err) //handle error

    fs.readFile(`./food_caches/${fileList[0]}`, (err, fileContents) => {
        if (err) //handle error

        try {
            const data = JSON.parse(fileContents);
            //handle succes
        } catch (err) {
            //handle error
        }
    });
});

Het onhandige is dat je nu op drie verschillende punten een mogelijke error moet afhandelen. Daarnaast moet je de synchrone en asynchrone errors die kunnen optreden ook nog op een ander manier behandelen.

Promises make this easier. They can be either resolved (the action finished successfully) or rejected (it failed). Resolve handlers (as registered with then) are called only when the action is successful, and rejections are automatically propagated to the new promise that is returned by then. And when a handler throws an exception, this automatically causes the promise produced by its then call to be rejected. So if any element in a chain of asynchronous actions fails, the outcome of the whole chain is marked as rejected, and no success handlers are called beyond the point where it failed.

Much like resolving a promise provides a value, rejecting one also provides one, usually called the reason of the rejection. When an exception in a handler function causes the rejection, the exception value is used as the reason. Similarly, when a handler returns a promise that is rejected, that rejection flows into the next promise. There’s a Promise.reject function that creates a new, immediately rejected promise.

To explicitly handle such rejections, promises have a catch method that registers a handler to be called when the promise is rejected, similar to how then handlers handle normal resolution. It’s also very much like then in that it returns a new promise, which resolves to the original promise’s value if it resolves normally and to the result of the catch handler otherwise. If a catch handler throws an error, the new promise is also rejected.

Hieronder zie je een het hetzelfde voorbeeld als hierboven, maar dan met promises.

readdirP('./food_caches').then(fileList => {
    return readFileP(`./food_caches/${fileList[0]}`)
}).then(fileContents => {
    const cacheInfo = JSON.parse(fileContents);
    //handle succes
}).catch(err => {
    //handle error
});

Het handige is dat je nu alle errors op één plek kunt afhandelen en op dezelfde manier.

As a shorthand, then also accepts a rejection handler as a second argument, so you can install both types of handlers in a single method call.   Deze notatie zie je bijna nooit

A function passed to the Promise constructor receives a second argument, alongside the resolve function, which it can use to reject the new promise.

Ter illustratie zie je hieronder de promise-based api voor readFile. Als er een error optreedt bij het lezen van de file dan wordt de functie reject uitgevoerd die het error-object in de meekrijgt.

readFileP = (file) => {
    return new Promise((resolve, reject) => {
        fs.readFile(file, 'utf8', (err, data) => {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
};

Deze implementatie zorgt ervoor een promise chain waarin readFileP gebruikt wordt, eventuele errors elegant kan afvangen met een functie in een `catch.

The chains of promise values created by calls to then and catch can be seen as a pipeline through which asynchronous values or failures move. Since such chains are created by registering handlers, each link has a success handler or a rejection handler (or both) associated with it. Handlers that don’t match the type of outcome (success or failure) are ignored. But those that do match are called, and their outcome determines what kind of value comes next—success when it returns a non-promise value, rejection when it throws an exception, and the outcome of a promise when it returns one of those.

new Promise((_, reject) => reject(new Error("Fail")))
  .then(value => console.log("Handler 1"))
  .catch(reason => {
    console.log("Caught failure " + reason);
    return "nothing";
  })
  .then(value => console.log("Handler 2", value));
// → Caught failure Error: Fail
// → Handler 2 nothing

Much like an uncaught exception is handled by the environment, JavaScript environments can detect when a promise rejection isn’t handled and will report this as an error.

De volgende, grijze tekst hoef je niet te lezen.
Lees verder waar de tekst weer zwart-op-wit wordt.
einde van tekst die overgeslagen kan worden
11.7

Collections of promises

De volgende, grijze tekst hoef je niet te lezen.
Lees verder waar de tekst weer zwart-op-wit wordt.
einde van tekst die overgeslagen kan worden

When working with collections of promises running at the same time, the Promise.all function can be useful. It returns a promise that waits for all of the promises in the array to resolve and then resolves to an array of the values that these promises produced (in the same order as the original array). If any promise is rejected, the result of Promise.all is itself rejected.

Oefening 11.7.1: Account Management Again (A)

In deze opgave bouwen we stap voor stap een promise chain op, waarmee we gebruikersgegevens lezen en wegschrijven naar bestanden.

De functie writeFileP(fileName, data) schrijft de gegevens uit de paramater data naar een bestand met de naam fileName en retourneert een promise. Hieronder zie je een voorbeeld waarin de username “ASH” naar een file wordt geschreven met de naam “ashlee_waters” en als dit gelukt is wordt er “done” op de console getoond.

const pw = require('../../promise-wrappers');

const user = {
    "account": "ashlee_waters",
    "username": "ASH"
};

pw.writeFileP(`${user.account}`, user.username).then(() => {
    console.log('done');
}).catch(err => {
    console.log(err.message);
});

In plaats van het wegschrijven van één gebruiker, willen we een hele array van gebruikers kunnen wegschrijven, waarbij we de username van elke gebruiker in een eigen file willen stoppen met de naam van het account .

Onderstaande array zou dus de drie files moeten: ‘ashlee_waters’, ‘hilario_muller’ en ‘serena_klein’ moeten opleveren.

const pw = require('../../promise-wrappers');

users = [
    {
        "account": "ashlee_waters",
        "username": "ASH"
    },
    {
        "account": "hilario_muller",
        "username": "Hilario_Muller29"
    },
    {
        "account": "serena_klein",
        "username": "Serena.Klein"
    }
];

Pas de meegegeven code zo aan dat je voor elke gebruiker uit de users array writeFileP aanroept.

Gebruik Promise.all om ‘done’ naar de console te schrijven als alle gebruikers zijn weggeschreven.

Zorg er ook voor dat je met een catch eventuele errors naar de console te schrijven.

Hint: writeFileP retourneert een promise, dus stop deze promise in een array en gebruik deze array als input voor Promise.all.

Oefening 11.7.2: Account Management Again (B)

We willen nu de gegevens van de gebruikers uit het bestand ‘users.json’ halen in plaats van uit de variabele users.

Begin de promise-chain die je bij opgave A) hebt gemaakt nu met een aanroep van readFileP waarmee je de gegevens uit ‘users.json’ haalt. Als de gegevens zijn gelezen, kun je beginnen met het wegschrijven van alle gebruikers zoals je bij opgave A) doet.

Zorg er ook nu voor dat je alle eventuele errors naar de console logt met een catch in de promise chain.

De volgende, grijze tekst hoef je niet te lezen.
Lees verder waar de tekst weer zwart-op-wit wordt.
einde van tekst die overgeslagen kan worden
11.12

The event loop

Asynchronous programs are executed piece by piece. Each piece may start some actions and schedule code to be executed when the action finishes or fails. In between these pieces, the program sits idle, waiting for the next action.

So callbacks are not directly called by the code that scheduled them. If I call setTimeout from within a function, that function will have returned by the time the callback function is called. And when the callback returns, control does not go back to the function that scheduled it.

Asynchronous behavior happens on its own empty function call stack Één stackDeze zin lijkt een beetje te suggereren dat er een nieuwe call stack gemaakt wordt voor elke aynchrone callback. Wij gaan er echter vanuit dat er maar één call stack is en dat een asynchrone callback op die stack geplaatst wordt, zodra deze stack leeg is. Zie ook het filmpje over de event loop hierboven. This is one of the reasons that, without promises, managing exceptions across asynchronous code is hard. Since each callback starts with a mostly empty stack, your catch handlers won’t be on the stack when they throw an exception.

try {
  setTimeout(() => {
    throw new Error("Woosh");
  }, 20);
} catch (_) {
  // This will not run
  console.log("Caught!");
}

No matter how closely together events—such as timeouts or incoming requests—happen, a JavaScript environment will run only one program at a time. You can think of this as it running a big loop around your program, called the event loop. When there’s nothing to be done, that loop is stopped. But as events come in, they are added to a queue, and their code is executed one after the other. Because no two things run at the same time, slow-running code might delay the handling of other events.

This example sets a timeout but then dallies until after the timeout’s intended point of time, causing the timeout to be late.

let start = Date.now();
setTimeout(() => {
  console.log("Timeout ran at", Date.now() - start);
}, 20);
while (Date.now() < start + 50) {}
console.log("Wasted time until", Date.now() - start);
// → Wasted time until 50
// → Timeout ran at 55

Promises always resolve or reject as a new event. Even if a promise is already resolved, waiting for it will cause your callback to run after the current script finishes, rather than right away.

Promise.resolve("Done").then(console.log);
console.log("Me first!");
// → Me first!
// → Done

In later chapters we’ll see various other types of events that run on the event loop.

De volgende, grijze tekst hoef je niet te lezen.
Lees verder waar de tekst weer zwart-op-wit wordt.
einde van tekst die overgeslagen kan worden
11.14

Summary

Asynchronous programming makes it possible to express waiting for long-running actions without freezing the program during these actions. JavaScript environments typically implement this style of programming using callbacks, functions that are called when the actions complete. An event loop schedules such callbacks to be called when appropriate, one after the other, so that their execution does not overlap.

Programming asynchronously is made easier by promises, objects that represent actions that might complete in the future, and async functions, which allow you to write an asynchronous program as if it were synchronous.

De volgende, grijze tekst hoef je niet te lezen.
Lees verder waar de tekst weer zwart-op-wit wordt.
einde van tekst die overgeslagen kan worden