Before discussing promises and why we need it, first take a look at this simple four-step calculation:
start with x = 5
step 1: multiply x by 2 (x = 10)
step 2: add 6 to x (x = 16)
step 3: subtract x by 1 (x = 15)
step 4: output x to the pre tag in the DOM (<pre>x = 15</pre>)
Open es6/promises/calculator_synchronous.html and es6/promises/calculator_synchronous.js
<pre></pre>
1
let x =5;
console.log(`x = ${x}`);// step 1 (x = x * 2)
x *=2;
console.log(`x * 2 = ${x}`);// step 2 (x = x + 6)
x +=6;
console.log(`x + 6 = ${x}`);// step 3 (x = x - 1)
x -=1;
console.log(`x - 1 = ${x}`);// step 4 (output x to DOM)
document.querySelector('pre').innerText =`x = ${x}`;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
The output looks like we expected
Let's do the same calculation, but this time:
stap 1 has a delay of two seconds
stap 2 has a delay of one second
Open es6/promises/calculator_asynchronous.html and es6/promises/calculator_asynchronous.js
let x =5;
console.log(`x = ${x}`);// step 1
console.warn('Give me two seconds, first empty my coffee and then I will multiply x by 2');setTimeout(()=>{
x *=2;
console.log(`x * 2 = ${x}`);},2000);// step 2
console.warn("Give me a sec, I'm coming down and add 6 to x");setTimeout(()=>{
x +=6;
console.log(`x + 6 = ${x}`);},1000);// step 3
x -=1;
console.log(`x - 1 = ${x}`);// step 4 (output x to DOM)
document.querySelector('pre').innerText =`x = ${x}`;
To explain the behavior of the previous example, we have to know the difference between synchronous and asynchronous functions in JavaScript
Javascript is a single threaded language:
This means it has only one call stack
A line of code must finish executing before it's moving to the next line of code
This is called synchronous
Most of the functions we already used are synchronous (e.g. console.log(), forEach(), while(), ...)
Some function in JavaScript can't give us a result immediately, gives us a result in time (e.g. fetch(), setTimeout(), setInterval(), DOM events, ...)
These functions are called asynchronous function and are treated differently
When a synchronous function is recognizes, this functions is taken out of the main thread, so the execution don't block, and the next line of code is executed
When the synchronous function is resolved, and when the main thread is finished, the result is sent back to the main thread
Asynchronous function are non-Blocking
Now the result of our previous example make sense:
let x =5;
console.log(`x = ${x}`);// step 1
console.warn('Give me two seconds, first empty my coffee and then I will multiply x by 2');setTimeout(()=>{
x *=2;
console.log(`x * 2 = ${x}`);},2000);// step 2
console.warn("Give me a sec, I'm coming down and add 6 to x");setTimeout(()=>{
x +=6;
console.log(`x + 6 = ${x}`);},1000);// step 3
x -=1;
console.log(`x - 1 = ${x}`);// step 4 (output x to DOM)
document.querySelector('pre').innerText =`x = ${x}`;
The two async functions are taken out of the main thread, so only this code is running first:
Line 11x is now 4
Line 15 the pre pag shows x = 4
let x =5;
console.log(`x = ${x}`);// step 1
console.warn('Give me two seconds, first empty my coffee and then I will multiply x by 2');// step 2
console.warn("Give me a sec, I'm coming down and add 6 to x");// step 3
x -=1;
console.log(`x - 1 = ${x}`);// step 4 (output x to DOM)
document.querySelector('pre').innerText =`x = ${x}`;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
At this point x = 4 and after one second the secondsetTimeout() function comes back on the main thread and will be executed
Remember that the secondsetTimeout() function has a delay of 1000ms and the firstsetTimeout() function has a delay of 2000ms so the second setTimeout() function wil be executed
before the first!
After that, x will be 10
x is already written to the pre tag, so our final result won't be visible in the DOM
We now can use REPL in the console to check the finale value of x
setTimeout(()=>{
x +=6;
console.log(`x + 6 = ${x}`);},1000);
1 2 3 4
At this point x = 10 and after another second the first setTimeout() function (witch has a delay of 2 seconds) comes back on the main thread and will be executed
After that, x will be 20
Again, that x is already written to the pre tag, and our new result won't be visible in the DOM
setTimeout(()=>{
x *=2;
console.log(`x * 2 = ${x}`);},2000);
1 2 3 4
The total execution of the whole task took about two seconds and this is the result
TIP
If you don't fully understand our (minimised) explanation of what async functions are, you can watch these movies and links with a more detailed explanation
What we really want is that the second setTimeout() function will be executed after the fitst setTimeout() and the last steps must be executed after the second setTimeout() function
With the knowledge we have so far, there is only one way to resolve our problem: nested callback function
Open es6/promises/calculator_callback.html and es6/promises/calculator_callback.js
As you can see, we have a lot of nested functions inside each other
This code is hard to read and hard to maintain, so they called it callback hell or the pyramid of Doom
let x =5;
console.log(`x = ${x}`);// step 1
console.warn('Give me two seconds, first empty my coffee and then I will multiply x by 2');setTimeout(()=>{
x *=2;
console.log(`x * 2 = ${x}`);// step 2
console.warn("Give me a sec, I'm coming down and add 6 to x");setTimeout(()=>{
x +=6;
console.log(`x + 6 = ${x}`);// step 3
x -=1;
console.log(`x - 1 = ${x}`);// step 4 (output x to DOM)
document.querySelector('pre').innerText =`x = ${x}`;},1000);},2000);
ES6 introduced a nicer, and a more elegant way to do async operations without the need of nested callbacks, called promises
Some features in JavaScript are already build upon promises (e.g. the fetch API) but sometimes you have to create your own promise like in previous example
A promise is just an object that can created with new Promise() and has three states:
pending: response is not ready yet, please wait....
fulfilled: response is ready, and the data can be used now (data is resolved)
rejected: an error occurred, and there is no data (data is rejected)
As we already know from fetch() chapter, the promise can be chained with one or more then() handlers to do something with the resolved data and one catch() handler that catches the (reject)
error
This is a basic skeleton for a promise that don't except any parameters:
const promise =newPromise((resolve, reject)=>{// do something that gives some data back after a certain timeif(/* everything turned out fine -> resolve the data */){resolve(data);}else{reject(newError('Oops, something went wrong'));}});
promise // without parentheses!.then(data=> console.log(data))// the data from the 'resolved' promise is available and can be used now.catch(error=> console.error(error));
1 2 3 4 5 6 7 8 9 10 11 12
This is a basic skeleton for a promise that except one or more parameters:
constpromise=(param1, param2)=>{returnnewPromise((resolve, reject)=>{// do something that gives some data back after a certain timeif(/* everything turned out fine -> resolve the data */){resolve(data);}else{reject(newError('Oops, something went wrong'));}});}promise(param1, param2)// with parentheses of course.then(()=> console.log('resolved'))// the data from the 'resolved' promise is available.catch(error=> console.error(error));
1 2 3 4 5 6 7 8 9 10 11 12 13 14
REMARK
If you only have to resolve a promise without sending data back, you can use resolve() instead of resolve(data)
There are two posible solutions to transform our calculation example from nested callbacks to promises:
Create two promises (one for every setTimeout() function) and return the result of the operation via resolve(...) inside the promise
Create only one promise (just a delay promise) and do the operations that are now inside the setTimeout() function in a then() handler after the timer is resolved
Let's refactor the calculator with one delay promise that we can use multiple times
The function delay(ms) expects one parameter (ms) with a default value of 1000ms
The function returns a promise:
the promise is rejected if the parameter ms in not a number (isNaN)
the promise is fulfilled after the given timeout and resolve a string that can be used in the next then(data) handler
let x =5;constdelay=(ms =1000)=>{returnnewPromise((resolve, reject)=>{if(isNaN(ms)){reject(newError('Delay only except milliseconds'));}else{setTimeout(()=>resolve(`waited for ${ms} ms`), ms);}});};
console.log(`x = ${x}`);// step 1
console.warn('Give me two seconds, first empty my coffee and then I will multiply x by 2');delay(2000).then((data)=>{
console.log(`%c ${data}`,'color: blue');
x *=2;
console.log(`x * 2 = ${x}`);})// step 2.then(()=> console.warn("Give me a sec, I'm coming down and add 6 to x")).then(()=>delay())// 1000 is the default value.then((data)=>{
console.log(`%c ${data}`,'color: blue');
x +=6;
console.log(`x + 6 = ${x}`);}).then(()=>{// step 3
x -=1;
console.log(`x - 1 = ${x}`);// step 4 (output x to DOM)
document.querySelector('pre').innerText =`x = ${x}`;}).catch((error)=> console.error(error));
If error checking is important, this is the function to use
constdelay=(ms =1000)=>{returnnewPromise((resolve, reject)=>{if(isNaN(ms)){reject(newError('Delay only except milliseconds'));}else{setTimeout(()=>resolve(`waited for ${ms} ms`), ms);}});};
1 2 3 4 5 6 7 8 9
If you omit to check if the parameter ms is a number, the code can be much shorter
constdelay=(ms =1000)=>{returnnewPromise((resolve)=>{setTimeout(()=>resolve(`waited for ${ms} ms`), ms);});};
1 2 3 4 5
Because each function within the curly braces contains only one statement, you may remove the curly braces when using arrow functions
constdelay=(ms =1000)=>newPromise((resolve)=>setTimeout(()=>resolve(`waited for ${ms} ms`), ms));
1
If you don't return any value when the timeout is fulfilled, the callback function for setTimeout() is the resolve callback function resolveitself
The lyrics for this karaoke are written in a heroes object
Every two second, a new song-line is written to div#lyrics
const lyrics = document.getElementById('lyrics');const heroes ={
line1:'<p><span>1</span> I, I will be king</p>',
line2:'<p><span>2</span> And you, you will be queen</p>',
line3:'<p><span>3</span> Though nothing will drive them away</p>',
line4:'<p><span>4</span> We can beat them, just for one day</p>',
line5:'<p><span>5</span> We can be heroes, just for one day</p>',};constdelay=(ms =2000)=>newPromise((resolve)=>setTimeout(resolve, ms));
lyrics.innerHTML +='<p>--- START --------------------------------------------</p>';delay().then(()=>{
lyrics.innerHTML += heroes.line1;returndelay();}).then(()=>{
lyrics.innerHTML += heroes.line2;returndelay();}).then(()=>{
lyrics.innerHTML += heroes.line3;returndelay();}).then(()=>{
lyrics.innerHTML += heroes.line4;returndelay();}).then(()=>{
lyrics.innerHTML += heroes.line5;returndelay();}).then(()=>{
lyrics.innerHTML +='<p>--- END ----------------------------------------------</p>';returndelay();}).then(()=> console.log('lyrics written to page'));
Select, with the range slider, the number of random words to select from the API
The fetch API don't need the new Promise because fetch() is already a promise by default
Line 12: Promise.all() expects an array of promises (words[]) that returns some kind of data, but the actual data from the fetch API is only available in the first then() handler
So the words[] array contains x times the promise fetch(wordUrl).then((response) => response.json())
Line 16 to 22: when all the data from every promise is resolved, we can loop over each element and append a definition to the dl tag
const number = document.getElementById('number');const dl = document.querySelector('dl');const wordUrl ='https://random-words-api.vercel.app/word';
number.addEventListener('change',()=>{
console.clear();
dl.innerHTML ='';const words =[];for(let i =1; i <= number.value; i++){// push the JSON response from the fetch API to the words[] array// remember that the JSON response is also a promise!
words.push(fetch(wordUrl).then((response)=> response.json()));}
Promise.all(words).then((responses)=>{// loop over all JSON responses once they have all been resolved
responses.forEach((response, index)=>{
console.log(`response ${index}`, response);
dl.innerHTML +=`<dt>${response[0].word}</dt><dd>${response[0].definition}</dd>`;});}).catch((error)=> console.error(error));});
number.dispatchEvent(newEvent('change'));