Up to this point, we have used JavaScript code to run simple web applications, which include: using variables, calling functions, and playing with the DOM. On functions, specifically, we even passed functions into another function (callback functions), and there's more to talk about this.
Let's start by stating that JavaScript is synchronous and single-threaded by definition, i.e: the code is executed from line 1 until the last one, one at a time and in order(ish). Take a look at this example:
1function runFirst(){ 2 console.log("first"); 3} 4function runSecond(){ 5 console.log("second"); 6} 7runSecond(); 8runFirst(); 9 10/* 11CONSOLE OUTPUT: 12 > second 13 > first 14*/
Here: line 5 runs before line 2 because we're calling runSecond()
(line 7) before runFirst()
(line 8). Breaking the order by commanding the computer to call (or execute) the code block within a function.
Things get more complicated when calling functions inside functions, as we can see here:
1function runFirst(){ 2 console.log("I want to run first"); 3 runSecond(); 4 console.log("I also want to run when runFirst runs"); 5} 6function runSecond(){ 7 console.log("Where am I running?"); 8} 9runFirst(); 10 11/* 12CONSOLE OUTPUT: 13 > I want to run first 14 > Where am I running? 15 > I also want to run when runFirst runs <-- This line of code had to wait for runSecond() to finish 16*/
OK What...?
This happens because the call stack in JavaScript keeps track of the functions that are currently running and being processed:
runFirst()
is pushed into the call stack because we called it (line 9).console.log
(line 2), after that, runSecond()
is called (line 3).runFirst()
pauses its execution and runSecond()
starts running.console.log
executed (line 7).runSecond()
finishes, runFirst()
starts again, executing the rest of its code, the last console.log
(line 4).F U N !
But wait, there's more... We could even pass a function as an argument to another function (nope, this is not a typo). The function sent as a parameter is called a callback function. Take a look:
1function runFirst(someFunction){ 2 console.log("I want to run first"); 3 someFunction(); 4 runSecond(); 5 console.log("I also want to run when runFirst runs"); 6} 7function runSecond(){ 8 console.log("Where am I running?"); 9} 10runFirst(aThirdOne); 11 12function aThirdOne(){ 13 console.log("This is crazy"); 14} 15 16 17/* 18CONSOLE OUTPUT: 19 > I want to run first 20 > This is crazy 21 > Where am I running? 22 > I also want to run when runFirst runs 23*/
...you may want to take a second look, don't worry, we'll wait...
Explanation time!
We've added a new function aThirdOne()
(line 12), which console-logs: "This is crazy"
; but we are not calling it directly, instead, we are passing its name as a parameter to runFirst()
(line 10). runFirst(someFunction)
it's now expecting a value (line 1) which will be called as if it were a function (line 3). Note that the name is different because we pass the value, not the variable name. This produces a new print in the console: "This is crazy"
before we call runSecond()
(line 4).
...jump around!, jump around!, jump around!, Jump up, jump up and get down!...
Now, let's assume that we need to load some files from a server, specifically images:
1function fetchingImages(){ 2 console.log("Load them!"); 3 // SOME CODE TO LOAD IMAGES 4 console.log("Images loaded!"); 5} 6function userIsWaiting(){ 7 console.log("I don't like waiting"); 8} 9fetchingImages(); 10userIsWaiting(); 11 12/*CONSOLE OUTPUT: 13 > Load them! // User starts waiting 14 // Now the user has to wait for the images to arrive; time: unknown... browser: frozen :( 15 > Images loaded! // After ?? seconds 16 > I don't like waiting // We don't want users to wait that long to see images 17*/
Unacceptable...
In a real life website, users will have to wait for a long time to see something, all because the DOM processing has to wait for the pictures to arrive from the server, and this is all because we are using the same thread of execution for everything.
Asynchronous programming is a way to process lines of code and handle the result without affecting the main thread.
1function fetchingImages(){ 2 console.log("Load them!"); 3 fetch("the_url_of_the_image").then( (response) => { 4 if(response.ok){ 5 console.log("Images Loaded!!"); 6 } else { 7 console.log("Uh-oh something went wrong"); 8 } 9 }); 10} 11function userIsWaiting(){ 12 console.log("I don't like waiting"); 13} 14fetchingImages(); 15userIsWaiting(); 16 17 18/*CONSOLE OUTPUT: 19 > Load them! // User starts waiting 20 > I don't like waiting // No waiting! DOM ready to see 21 // ... and ?? seconds later 22 > Images loaded! OR Uh-oh something went wrong // Images!... Magic! || Oops, no images 23*/
JavaScript offers a handful of predefined asynchronous functions that we can use to solve any possible scenario. Some of them are:
In this case, we used the Fetch API to load the images, and then (after getting an answer from the backend) we wrote some feedback from the process.
Keep in mind that any network call could fail for many reasons, and we should always be prepared for failure.
A promise is nothing more than the result of an asynchronous operation. It represents the completion or failure of that result in the form of the object provided by the promise.
This is how a promise can be created:
1let myPromise = new Promise(function(resolve, reject) { 2 setTimeout(function() { 3 resolve("I was resolved"); 4 }, 300); 5}); 6myPromise.then((obj) => { 7 console.log(obj); 8}); 9console.log(myPromise); 10 11/*CONSOLE OUTPUT: 12 > [promise object] // It will return a promise object 13 > I was resolved 14*/
1// Here Promise represents the Promise object. 2Promise.resolve("I was resolved with this value").then(value => console.log(value)); 3 4/*CONSOLE OUTPUT: 5> I was resolved with this value 6 7*********** 8 A better approach will be to initialize a variable 9 equals to the resolved Promise 10 11--- sample: 12 let myResolvedPromise = Promise.resolve("I was resolved with this value"); 13*/
1Promise.reject(new Error("I was rejected")).catch(error => console.log(error));
1let promise = new Promise(function(resolve,reject){ 2 resolve("I was resolved, and you can see me when you use the then method"); 3}); 4promise.then(value => console.log(value));
1let promise = new Promise(function(resolve,reject){ 2 reject("I was rejected, and you can see me when you use the catch method"); 3}); 4promise.catch(error => console.log(error));
☝ Remember that await expressions are only valid inside async functions. If you use them outside, you will have a syntax error.
1function returnedPromiseHere() { 2 return new Promise((resolve, reject) => { 3 setTimeout(() => { 4 resolve("I am the images coming from the database"); 5 }, 1000); 6 }); 7} 8async function useAsyncFunction() { 9 console.log("I am a fast task"); 10 let result = await returnedPromiseHere(); 11 console.log(result); 12 console.log("I had to wait for await to finish"); 13} 14useAsyncFunction(); 15 16/*CONSOLE OUTPUT: 17 > I am a fast task 18 // After 1 second... 19 > I am the images coming from the database 20 > I had to wait for await to finish 21*/
1function promise1() { 2 return new Promise((resolve, reject) => { 3 setTimeout(() => { 4 resolve("I am resolved as 1"); 5 }, 100); 6 }); 7} 8function promise2() { 9 return new Promise((resolve, reject) => { 10 setTimeout(() => { 11 resolve("I am resolved as 2"); 12 }, 200); 13 }); 14} 15function promise3() { 16 return new Promise((resolve, reject) => { 17 setTimeout(() => { 18 resolve("I am resolved as 3"); 19 }, 300); 20 }); 21} 22async function handlingAllPromises() { 23 let first = await promise1(); 24 let second = await promise2(); 25 let third = await promise3(); 26 27 console.log(first); 28 console.log(second); 29 console.log(third); 30} 31handlingAllPromises();
☝ In the example above, instead of awaiting for a promise on every new line, we could use the
Promise.all
method and wait for all the promises to be fulfilled.
1let [first, second, third] = await Promise.all([promise1(), promise2(), promise3()]);
1const handlingAllPromises = async () => { 2 let [first, second, third] = await Promise.all([promise1(), promise2(), promise3()]); 3 4 console.log(first); 5 console.log(second); 6 console.log(third); 7}
A good way of handling errors in async functions is to use the try...catch
statements.
1async function handeErrors() { 2 let msg; 3 try { 4 msg = await promise1(); // Notice this method is already written in your application 5 console.log(msg); 6 } catch(err) { 7 console.log(err); 8 } 9}
1async function fetchData(endpoint) { 2 const response = await fetch(endpoint); // Notice the use of fetch API 3 let data = await response.json(); 4 data = data.map(user => user.ID); 5 console.log(data); 6} 7 8fetchData(http://dummyData/api/allUsers); // This is an example endpoint 9 10/*CONSOLE OUTPUT: 11 > [1, 2, 3, 4] // Here we get all users ID from the database 12*/
You have the ability to create awesome and faster web applications. In addition, users and faster tasks no longer need to wait for slow tasks to be finished, thanks to asynchronous programming.