As discussed before, the JavaScript engine runs inside an execution environment such as the Browser or Node.js.
Depending on what this environment is, the JS is exposed to several APIs, including the DOM (if it's the browser) or the Timers API (on both).
JavaScript has a non-blocking I/O model that runs on a single threaded system.
Considering the statement above, which remains true, what will happen when our web application needs to make a call to an external API?
Timeout
JavaScript isn't capable of achieving concurrency via parallel code execution because it's a single threaded language.
To solve this, the language implements a concurrency model based on an event loop.
JavaScript's engine call stack runs synchronously. However, API calls run asynchronously.
But how?
Browser APIs operations can run concurrently using a small number of threads. These operations result in a callback function that is sent to the callback queue until it can be executed.
When the call stack is empty, the event loop takes the first callback from the event queue and places it in the call stack for execution.
We'll explore the exception to this rule later.
JavaScript's Concurrency Model
Remember the callback queue and the event loop?
The event loop will fetch callback functions from the queue in a FIFO order. There's only one exception to this rule: rendering events.
Rendering events have higher priority for execution and will be fetched from the queue first!
function toBeExecuted(arg) {
while (true) { }
}
What will happen in this case?
The event loop will not fetch anything from the queue
if the call stack is not empty!!
JavaScript functions are called in the order they are invoked in code.
Functions may invoke other functions as part of their operation, or at the end of their execution.
The function to be invoked may be passed to the outer function as an argument - callback function.
function1(){
console.log("World!");
}
function2(){
console.log("Hello");
}
function2();
function1();
//Result in console:
//Hello
//World!
function1(){
console.log("World!");
}
function2(){
console.log("Hello");
function1();
}
function2();
//Result in console:
//Hello
//World!
function1(){
console.log("World!");
}
function2(callback){
console.log("Hello");
callback();
}
function2(function1);
//Result in console:
//Hello
//World!
Callback functions are functions that are passed as an argument to another (outer) function. The outer function invokes the callback function as part of its operation.
A very common use of callback functions is with event listeners.
function onBtnClicked() {
//Logic to execute when button with
//id="buttonA" is clicked
}
let btnA = document.querySelector('#buttonA');
btnA.addEventListener('click', onBtnClicked);
Callback function in event listener
Callback functions may be passed:
function1(){
console.log("World!");
}
function2(callback){
console.log("Hello");
callback();
}
//reference (name only)
function2(function1);
//anonymous function, traditional expression
function2(function(){console.log("Mindswap!")});
//anonymous function, arrow function expression
function2(()=>console.log("Students!"));
When using callback functions, the outer functions are not required to know exactly what function it needs to invoke (only its arguments, if any).
For this reason, callback functions can be useful to better separate repeatable code.
function printMessage(arg){
console.log(arg);
}
function exampleFunction(arg) {
let a = ...;
printMessage(arg + a);
//...
}
exampleFunction("Hello ");
exampleFunction("Goodbye ");
function helloMessage(arg) {
console.log('Hello ' + arg);
}
function byeMessage(arg) {
console.log('Goodbye ' + arg);
}
function exampleFunction(callback) {
let a = ...;
callback(a);
//...
}
exampleFunction(helloMessage);
exampleFunction(byeMessage);
The main advantage of callback functions, however, is for asynchronous use.
Callbacks can be used to execute some action when an asynchronous operation has finished.
This allows to define the desired operation without having to block other operations while waiting.
function printMessage() {
console.log("Time's up!");
}
console.log("Starting a 5-second timer...");
//Other operations...
setTimeout(printMessage, 5000);
Note: setTimeout() is a method that sets a timer that executes a functions after a defined number of milliseconds.
Another related method is setInterval() which executes a function every X milliseconds.
Some asynchronous operations are dependent on the execution of other asynchronous operations.
This may lead to nested callback functions, where a callback function calls the next asynchronous function and passes its callback function, and so on.
Nesting callbacks can very easily create what is often called Callback Hell or Pyramid of Doom.
Callback hell can be avoided with some simple steps, such as:
Another solution is to move away from the callback pattern and use Promises instead.
JavaScript promises are objects that link a "producing code" - code that takes time executing - and the "consuming code" - code waiting for the result.
Promises have a state, which can be:
When the promise is fulfilled, it returns a value (result), and when it is rejected, it returns an error (reason).
In order to handle the returned result of a promise, callback (or handler) functions are attached to the Promise object.
The .then() and .catch() methods are used to add the callback functions to the promise. Even if it has already been settled (fulfilled or rejected) before the callback functions are added, they will still be invoked.
function functionA(value){ //What to do when promise is fulfilled }
function functionB(error){ //Error handling for promise }
let myPromise = new Promise((successFunction, rejectFunction) => {
successFunction(...); //Calling success callback
//....
rejectFunction(...); //Calling error handling callback when an error is found
});
myPromise.then(functionA, functionB); //then() method receives 1 or two arguments
//or instead
myPromise.then(functionA).catch(functionB);
Error handling functions should be provided, to handle any possible thrown error in the chain. For simplicity, a promise chain may have only one catch() handler at its end.
Promise chaining, compared to callback nesting, allows for a more readable and organized code.
doSomething(function(result) {
doSomethingNext(result, function(newResult) {
doSomethingAfter(newResult, function(newerResult) {
doSomethingFinally(newerResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
}, failureCallback);
doSomething()
.then(result => doSomethingNext(result))
.then(newResult => doSomethingAfter(newResult))
.then(newerResult => doSomethingFinally(newerResult))
.then(finalResult => {
console.log('Got the final result: ' + finalResult);
})
.catch(error => failureCallback(error))
Most current Web APIs return promises with their asynchronous methods (see Fetch API example below).
Some, however, still use the callback function pattern. These can be wrapped with the Promise() constructor, to take advantage of its functionalities.
// Requesting a resource from
// the network with Fetch API.
let myRequest = new Request('flowers.jpg'); //requesting asset from network
fetch(myRequest)
.then(function(response) {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.blob();
})
.then(function(response) {
//...
});
// The setTimeout() method
// does not support promises.
let myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Hello World');
}, 300);
});
Callback Functions/Promises
We are going to create a web page that starts a visual countdown and gets a magic number and performs a quick multiplication.
Create a simple HTML page, with a number input element, and two buttons. The number input will be used to indicate the number to be used for the multiplication. One of the buttons will be for starting the 5-second countdown and the other to stop it.
Additionally, create three empty paragraph (or similar) elements. The first one will display the countdown value, the second will display the magic number, and the third will show the result of the multiplication.
Callback Functions/Promises
Add a script to the HTML page that will:
We can create asynchronous functions, based on promises, using a combination of two keywords: async and await.
Declaring a function with the async keyword turns that function asynchronous.
These async functions return a promise, which resolves with the value returned by the function, or rejects with an error.
//Declaring async function
async function asyncFunction() {
//...
var val = await ...; //Wait for asynchronous function
//The code after 'await' is only executed
//when its promise is resolved
}
//Function expression
var funct1 = async function(){
//...
}
//Arrow function
var funct2 = async () => {/***/}
asyncFunction();
funct1();
funct2();
The await keyword can only be used in async functions. As the word indicates, it is used to wait for a promise.
This keyword is applied before invoking a promise-based function.
When it is used, the surrounding async function will halt its operations until the "awaited" async function resolves its promise.
It returns the value of the promise, if fulfilled, or it throws the error if rejected.
//Declaring async function
async function asyncFunction() {
//The code until the first use of await is run synchronously
var val = await new Promise((success, reject)=>{...})
//The execution of this function is halted until
//the provided promise resolves.
}
asyncFunction();
As the usage of these keywords involve Promises, we can use .then() and .catch() as well.
We can attach then/catch to the async function, which will be called once it has been resolved. We can also create promise chains, but in a simpler way.
We can also use these functions inside the async function, with the promise-based functions that are being "awaited".
For example, we could handle thrown errors with catch() instead of using try...catch.
Inside an async function, the await operator can be used multiple times throughout its operation.
When we want to wait for several promises before continuing the operations, we can use Promise.all() and other similar functions.
async function mainFunction() {
let var1 = await asyncFunction1(...);
let var2 = await asyncFunction2(...);
let var3 = await asyncFunction3(...);
// Each promise will wait for the previous to be
// settled before executing (instead of being processed simultaneously)
}
async function mainFunction() {
let var1 = asyncFunction1(...);
let var2 = asyncFunction2(...);
let var3 = asyncFunction3(...);
let values = await Promise.all([var1, var2, var3]).catch(...);
//The execution continues once all promises have been fulfilled
}
The Promise object provides functions that receive an array of Promises and returns a promise that:
Using any of these functions instead of awaiting for each Promise individually allows for the Promises to be processed simultaneously.
As you might have noticed by now, we can use await to halt operations and run code after each settled promise.
For this reason, we can use async/await to simplify code that uses promises and promise chains.
//Example from Promises' class
let myRequest = new Request('flowers.json');
fetch(myRequest)
.then(function(response) {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(function(json) {
//JSON object can be used now
console.log(json.title);
})
.catch(e => {
console.log(/*Print out any errors*/);
});
async function fetchFunction() {
let myRequest = new Request('flowers.json'); //requesting asset from network
let response = await fetch(myRequest);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
let json = await response.json();
console.log(json.title);
}
fetchFunction()
.catch(e => {console.log(/*Print out any errors*/)});
COPY
As you might have noticed by now, we can use await to halt operations and run code after each settled promise.
For this reason, we can use async/await to simplify code that uses promises and promise chains.
//Example from Promises' class
let myRequest = new Request('flowers.json');
fetch(myRequest)
.then(function(response) {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(function(json) {
//JSON object can be used now
console.log(json.title);
})
.catch(e => {
console.log(/*Print out any errors*/);
});
async function fetchFunction() {
let myRequest = new Request('flowers.json'); //requesting asset from network
let response = await fetch(myRequest);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
let json = await response.json();
console.log(json.title);
}
fetchFunction()
.catch(e => {console.log(/*Print out any errors*/)});
COPY
Convert our callback and promises exercise from last week, to use async/await instead of a promise chain.