5. Fetch & Promises
5. Fetch & Promises
Tot nu toe was alle code waarmee we gewerkt hebben synchroon, dit wil zeggen dat de code uitgevoerd wordt in de exacte volgorde waarin deze neergeschreven is. Dit hoofdstuk bespreekt asynchrone code, code waar de uitvoervolgorde eventueel kan wijzigen. Tenslotte gebruiken we asynchrone code om data op te halen van een API en te renderen in een website.
Asynchrone code
Alhoewel de meeste code onmiddellijk een resultaat moet en kan geven, zijn er verschillende cruciale operaties die niet ogenblikkelijk een resultaat kunnen teruggeven. Denk hier bijvoorbeeld aan het ophalen van data uit een database of van een API, het werken met bestanden (IO) of gewoon code die elke seconden iets moet doen (zoals een timer die bijhoudt hoelang je al met een kruiswoordraadsel).
JavaScript kan slechts één statement per keer verwerken, normaal gezien is dit geen probleem omdat de statements zodanig snel verwerkt kunnen worden dat we niet merken dat de code met iets anders bezig is. We stoten echter op problemen als we langdurige taken moeten uitvoeren.
Stel dat een gebruiker vraag om data te verversen in een dashboard, JavaScript moet dan een HTTP GET request sturen naar een API-endpoint om deze op te halen. Aangezien we over een dashboard spreken, kan de hoeveelheid data ook aanzienlijk zijn, bijgevolg duurt dit request lang. Als het request synchroon zou zijn, zou de volledige applicatie blocked zijn tot dit request afgewerkt is, de gebruiker zou dan niet meer kunnen doen behalve wachten. Dit is duidelijk een heel slechte user-experience.
In plaats van blocking synchrone code te schrijven maken we gebruik van asynchrone code, zou kan de gebruiker in bovenstaand voorbeeld de website blijven gebruiken terwijl de data ingeladen wordt.
Asynchrone code kan geschreven worden via callbacks of promises. Nieuwere code maakt voornamelijk gebruik van promises terwijl oudere (legacy) code gebruik maakt van callbacks. Dat wil niet zeggen dat callbacks nooit meer gebruikt worden, onderstaand voorbeeld geeft een callback mee aan de setTimeout methode die een boodschap wegschrijft naar stdout wanneer de timeout verlopen is. De uitvoer toont duidelijk dat de volgorde waarin de code uitgevoerd wordt kan variëren, zelfs van run tot run.
for (let i = 0; i < 10; i++) {
// Callback die na een willekeurige tijd wordt uitgevoerd.
setTimeout(() => {
console.log(`Asynchronous operation ${i + 1} completed`);
}, Math.random() * 1000);
}Asynchronous operation 5 completed
Asynchronous operation 2 completed
Asynchronous operation 1 completed
Asynchronous operation 7 completed
Asynchronous operation 3 completed
Asynchronous operation 8 completed
Asynchronous operation 4 completed
Asynchronous operation 9 completed
Asynchronous operation 6 completed
Asynchronous operation 10 completedPromises
Een promise is een belofte dat een methode ergens in de toekomst een resultaat zal teruggeven. Het belangrijkste deel van voorgaande zin is "ergens in de toekomst", de methode voert geen onmiddellijke operatie uit. Het is mogelijk dat het resultaat na enkele milliseconden beschikbaar is, maar het kan evengoed meerdere seconden duren.
Promises bevinden zich steeds in één van drie mogelijke statussen:
- Pending: De defaultstate, de promise is noch vervuld, noch afgewezen.
- Fulfilled: De promise is vervuld, de asynchrone code is volledig zonder fouten afgehandeld.
- Rejected: De asynchrone code in de promise heeft ergens een fout vertoond en is niet succesvol afgehandeld.

Promise aanmaken
Een promise kan heel eenvoudig aangemaakt worden via de constructor van de promise klasse. Deze constructor heeft één argument, een functie met een resolve en reject parameter, de body van deze functie doet iets asynchroon en gebruikt de resolve parameter om het resultaat door te geven en de status te wijzen naar fulfilled of de reject parameter om de status te wijzigen naar rejected.
// Een promise die altijd in de pending state zit.
const promise = new Promise((resolve, reject) => {
// Asynchrone operaties.
})
// Een promise die vrijwel onmiddelijk resolved, de body
// bevat geen asynchrone operaties.
const promise = new Promise(resolve => {
const currentDate = new Date()
resolve(currentDate)
})
// Een promise die vrijwel onmiddelijk resolved, willekeurig succesvol of mislukt.
const promise = new Promise((resolve, reject) => {
const n = Math.floor(Math.random() * 10)
if (n > 5) {
resolve(n)
} else {
reject(new Error('The promised failed'))
}
})then, catch en finally
De then, catch en finally instantie-methodes kunnen gebruikt worden om het resultaat van een promise op te vragen.
De then methode krijgt het resultaat van een fullfilled promise als argument, de catch methode het error-object waarmee en promise rejected is en de finally methode wordt uitgevoerd nadat een promise afgehandeld is (al dan niet succesvol). Elk van deze methodes kan individueel, of in combinatie met de andere gebruikt worden.
const promise = new Promise((resolve, reject) => {
const n = Math.floor(Math.random() * 10)
if (n > 5) {
resolve(n)
} else {
reject(new Error('The promised failed'))
}
})
promise
.then(result => console.log(`The promise resolved with ${result}`))
.catch(error => console.error(error))
.finally(() => console.log('Example promise completed.'))1 | const promise = new Promise((resolve, reject) => {
2 | const n = Math.floor(Math.random() * 10)
3 | if (n > 5) {
4 | resolve(n)
5 | } else {
6 | reject(new Error('The promised failed'))
^
error: The promised failed
at <anonymous> (/Users/sebastiaan/Downloads/test.ts:6:20)
at new Promise (1:11)
at /Users/sebastiaan/Downloads/test.ts:1:17
at loadAndEvaluateModule (2:1)
Example promise completed.The promise resolved with 5
Example promise completed.Het resultaat van then is steeds weer een nieuwe promise, daaraan kan dus weer een nieuwe then methode gekoppeld worden.
promise
.then((resultA) => {/* Do something and return a new result. */})
.then((resultB) => {/* Do something and return a new result. */})
.then((resultC) => {/* Do something and return a new result. */})
.catch(error => {
// Reageer op elke error, in de eerste, tweede, derde of vierde promise.
})
.finally(() => console.log('De vierde promise is successvol afgehandeld.'))all en race
Het is regelmatig nodig om data op te halen uit verschillende bronnen, af en toe moet alle data uit alle bronnen geladen zijn voordat de pagina getoond mag worden. In dat geval kan de Promise.all methode gebruikt worden. Deze methode creëert een nieuwe promise die resolved als alle argumenten ook resolved zijn, indien één van de argumenten rejected wordt, wordt de volledige nieuwe promise ook rejected.
const combinedPromise = Promise.all([
new Promise(r => setTimeout(() => r("Hello, World!"), 2000)),
new Promise(r => setTimeout(() => r("Not again :("), 1000)),
// Zet onderstaande regel in commentaar om de foutmelding te voorkomen.
// new Promise( (_, reject) => setTimeout(() => reject("Whoops error"), 3000))
])
combinedPromise
.then(results => results.forEach(x => console.log(x)))
.catch(console.error)De Promise.race methode maakt ook een nieuwe promise aan, maar wacht slechts tot één van de argumenten resolved is.
async/await
Hierboven hebben we de then en catch methodes besproken, alhoewel deze werken wordt code die een ketting van verschillende promises gebruikt snel onoverzichtelijk. De async en await keywords zijn syntactische suiker rond de Promise klasse.
Elke functie die als async aangeduid wordt, geeft een promise terug. Onderstaande fragmenten produceren dus twee keer hetzelfde resultaat.
// Optie één via de Promise klasse.
const version1 = new Promise((resolve, reject) => {
const n = Math.floor(Math.random() * 10)
if (n > 5) {
resolve(n)
} else {
reject(new Error('The promised failed'))
}
})
// Optie twee via de async syntactische suiker.
async function version2() {
const n = Math.floor(Math.random() * 10)
if (n > 5) {
return n
} else {
throw new Error('The promise failed')
}
}Het is duidelijk dat de async versie beter leesbaar is en meer overeenkomt met de code die we tot nu toe gewoon zijn. De voordelen worden nog duidelijker als we ook gebruik maken van het await keyword. Via dit keyword kunnen we wachten op een resultaat van een promise of asynchrone functie, het komt dus overeen met de then methode die hierboven besproken is.
Als een asynchrone functie een await tegenkomt, wordt dit behandeld als een promise. De functie wordt "gepauzeerd" totdat de promise afgehandeld is, vervolgens gaat de code verder vanaf de lijn onder de await. Terwijl de functie "gepauzeerd" is, kan andere code uitgevoerd worden.
Om foutmeldingen af te handelen kan de catch methode van hierboven vervangen worden met een klassieke try-catch structuur.
async function asyncExample() {
const shouldReject = Math.floor(Math.random() * 10) % 2 === 0
if (shouldReject) {
return 'Succesvol afgerond!'
} else {
throw new Error('Er is iets fout gegaan...')
}
}
asyncExample()
.then(console.log)
.catch(error => console.error('Error: ', error.message))
async function handleAsyncExample() {
try {
const result = await asyncExample()
console.log(result)
} catch (error) {
console.error('Error: ', error.message)
}
}
handleAsyncExample()Fetch
Fetch is ingebakken in elke browser en elke runtime (Node, Bun, Deno), en kan dus gebruikt worden zonder extra libraries of scripts te installeren.
Begrip: fetch
De fetch methode kan gebruik worden om informatie op te halen of weg te schrijven naar een API.
De methode heeft één verplichte parameter, de URL die aangeroepen moet worden. Verder kan optioneel een HTTP-methode (GET, POST, PUT, PATCH, DELETE), body of header toegevoegd worden.
fetch(
'http://example.com',
{
// OPTIONEEL: De HTTP-methode, GET is de default.
method: 'GET',
// OPTIONEEL: De body van het request.
// Enkel beschikbaar voor POST, PUT en PATCH.
// Meestal een JSON-Object, maar kan ook een Blob of FormData zijn.
body: JSON.stringify({}),
// OPTIONEEL: Extra HTTP-headers.
headers: {
'Content-Type': 'application/json',
'Api-Key': 'amkqjq24lnsfnpiohz1',
},
},
)Het resultaat van een fetch-call is steeds een promise van een Response object. Het result-object heeft onder andere volgende properties:
status: De HTTP Status Code, 200 voor success, 500 voor een internal server error, ...text(): Een methode die het antwoord verwerkt en teruggeeft als een promise van plain-text.json(): Een methode die het antwoord verwerkt en teruggeeft als een promise van een JavaScript object.blob(): Een methode die het antwoord verwerkt en teruggeeft als een promise van een blob (een binary large object). Dit kan bijvoorbeeld gebruikt worden voor afbeeldingen, pdf bestanden, Word bestanden, ...
Onderstaande code doet een GET request naar de jsonplaceholder api. Dit is een gratis service die mock-data teruggeeft voor alle HTTP-methodes, voor meer informatie over de beschikbare endpoints, verwijzen we naar de website.
Eens de promise die door de fetch methode terug gegeven is afgehandeld is, moeten we het resultaat nog verwerken. Het Response object bevat de body als een stream van data (bytes), deze moeten verwerkt worden als tekst, een JSON-object[1] of een blob[2]. De Response klasse voorziet hier respectievelijk de text, json en blob methodes voor.
Eens het antwoord van de API verwerkt is en geconverteerd naar een JavaScript object, kunnen we de resulterende array gebruiken om elke post te renderen, zoals besproken in het vorig hoofdstuk.
async function renderPosts() {
const result = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await result.json();
const postContainer = document.getElementById('posts');
posts.forEach(post => postContainer.appendChild(createPost(post)));
}
renderPosts()
function createPost(post) {
const postCard = document.createElement('div');
// ... Wegelaten uit dit fragment.
return postCard
}Via het netwerktabblad in de dev-tools, kunnen we het resultaat van de fetch-call bekijken. Zowel het verstuurde request als het ontvangen response zijn daar zichtbaar.
Post request
De JSON Placeholder API kan ook gebruikt wordt om het aanmaken van een nieuwe post te simuleren. Om een resource aan te maken, moeten we een POST request gebruiken, dit wordt aangegeven via de method parameter.
Natuurlijk moet de server ook weten wat aangemaakt moet worden, hiervoor gebruiken we de body parameter, merk op dat het JavaScript object met de data eerst geconverteerd moet worden naar een JSON-string via de stringify methode.
Een HTTP request moet aan de server aangeven wat voor data er verstuurd wordt, anders kan de server het request niet correct afhandelen. Via de Content-Type header specifiëren we het MIME-type van de ingezonden data. In dit geval dus application/json.
fetch(
'https://jsonplaceholder.typicode.com/posts',
{
method: 'POST',
body: JSON.stringify({
title: 'Mijn titel',
body: 'Lorem Ipsum is slechts een proeftekst uit het drukkerij- en zetterijwezen.',
userId: 99,
}),
headers: {
'Content-Type': 'application/json',
}
}
)Ook voor een POST request is alle informatie zichtbaar in de dev-tools.
Voorbeeldcode
Uitgewerkt lesvoorbeeld met commentaar
JSON staat voor JavaScript Object Notation en is de de facto standaard voor het uitwisselen van gegevens tussen applicaties (ook voor andere talen). Elk JavaScript object kan eenvoudig omgezet worden naar JSON en omgekeerd via de statische methodes uit de JSON klasse. ↩︎
BLOB staat voor Binary Large Object, een representatie van een binair object zoals een afbeelding, video, document, zip-file, executable, ... ↩︎