Week 9 - Promises & Async
This week we will be learning about the JavaScript event loop and how the sequencing of code happens internally in the JavaScript engine.
We will discuss:
- asynchronous and synchronous code
- execution contexts
- promises
- tasks and microtasks
- timers
Async vs Sync
Section titled “Async vs Sync”Almost all the code that we have written to this point has been synchronous code. That means that you write the code in the order that you expect it to run. The first line of code inside your function will run and complete before the second line of code.
However, there is also Asynchronous code in JavaScript. Promises
are an example of this.
When you write code that will take an unknown amount of time to complete but you don’t want your App to freeze and do nothing until that one task is complete, then you need to use Async tasks.
JavaScript has a mechanism known as the event loop. When you pass a list of commands to the event loop, it will keep running each of those commands in order until it gets to the end of your list. This list is known as the main stack. JavaScript keeps trying to run commands as long as there is something on the main stack.
However, sometimes there is a command that would stop the event loop from getting to the next command. It gets blocked from going to the next command in the stack. These types of commands are things that take a long time to complete, like talking to a database, or the file system, or a network request for a file, or a timer that needs to wait before running.
There are specific tasks that are going to take too long and they get put into a secondary area so they can wait for their result. These tasks are known as asynchronous.
If the code stays on the main stack and insists on being run before the event loop moves on to the next item then it is called synchronous.
Async vs Sync
Promises
Section titled “Promises”A Promise, as the name implies is something that Promises a return value. We just don’t know when we will get the reply. It could be fulfilled or it could fail to be fulfilled. Either way you will get an answer.
A Promise that is fulfilled and returns what we want is called resolved
.
A Promise that is not fulfilled is called rejected
.
The syntax for a Promise is just like the fetch
method, which we will be learning next week. (That is because a fetch IS a Promise). When you call fetch()
you are actually given a Promise object which eventually will give you a response object and call the first then
method or it fails and calls the catch
method.
//version one that runs as soon as the current main stack is finishedlet p = new Promise(function (resolve, reject) { resolve('Booya');});p.then(function (str) { console.log(str); //outputs `Booya`}).catch(function (err) { //runs if the Promise function or the then function throws an error});
//version two with a minimum 2 second delaylet delay = new Promise(function (resolve, reject) { setTimeout(resolve, 2000, 'hello'); //wait for two seconds and then call the resolve});//at this point in your code delay is `pending` - not resolved or rejecteddelay.then(function (response) { //response will be the value returned by the resolve method inside the Promise console.log(response); //it will output 'hello'});
delay.catch(function (err) { //if the reject method was called first from the Promise //then this catch method's function would run.});//this last line will run before ANY of the other console.logsconsole.log('wait for it...');
The then
and catch
can be added to the variable holding the Promise separately, or they can be chained together with the catch
at the end of the chain.
Promise basic syntax
Promise Gotchas
then() and catch() and finally()
Section titled “then() and catch() and finally()”When you create a Promise
object you can chain onto it one or more then()
methods. At the very end of the then()
chain, you can put a catch()
method.
new Promise(func1).then(func2).then(func3).then(func4).catch(func5);//orlet p = new Promise(func1);p.then(func2).then(func3).then(func4).catch(func5);//notice that we can just put the names of the functions to call inside each
Just like the Promise object itself needs a function and that function will resolve or reject, each of the then()
method calls plus the catch()
method call needs a function.
If the function inside a then()
method runs with no errors, then its return value gets passed along to the next then()
method in the chain. The first return value becomes an argument for the second then()
, and so on.
If ANY of the functions inside ANY of the then()
methods throws an error, then the Error
object will be immediately passed to the catch()
method and the function inside the catch
will run using the Error
as its argument.
Converting Callbacks to Promise
Finally method
resolve() and reject()
Section titled “resolve() and reject()”It is possible to create a Promise that resolves or rejects immediately. However, since Promises are asynchronous it means that the current stack finishes first. The then()
method, which reads the value from the resolved or rejected promise is called asynchronously.
The Promise.resolve()
method returns a Promise object that has been resolved.
The Promise.reject()
method returns a Promise object that has been rejected.
let myGoodValue = 42;let myBadValue = new Error('bad stuff');
let good = Promise.resolve(myGoodValue);let bad = Promise.reject(myBadValue);
Promise.all() and allSettled() - Every Result
Section titled “Promise.all() and allSettled() - Every Result”If you have multiple asynchronous tasks to accomplish and you want to wrap them together as a Promise you can do that. As an example - you want to fetch remote data from 4 sources or you want to open 4 files from the filesystem. Either of those things take an unknown amount of time and depend on factors that are out of your control.
We can use Promise.all()
or Promise.allSettled()
to achieve this. Both of these methods take an Array of Promise objects and then return an Array of results to your then()
method.
The difference between them is that allSettled()
will call the then()
when every one of the Promises has a result. You don’t know if the results are all resolved, all rejected, or a mixture. With all()
the then()
method gets called only if all the promises in the Array were resolved. As soon as one of the Promises in the Array is rejected, the catch
gets called.
So, it is a question of how many results do you need. If you ask for four things to work but are ok with only two or three actually working, then go ahead and use all
.
If your app needs all four to have been successful, then use allSettled
.
//creating a couple promises that will randomly resolve or rejectlet p1 = new Promise((resolve, reject) => { //roughly half of the time this resolves let num = Math.random(); if (Math.round(num)) { resolve(num); } else { reject(num); }});let p2 = new Promise((resolve, reject) => { //roughly half of the time this resolves let num = Math.random(); if (Math.round(num)) { resolve(num); } else { reject(num); }});
//test the Promise.allSettled methodPromise.allSettled([p1, p2]) .then((results) => { //results is the array of numbers that resolved console.log(results[0], results[1]); }) .catch((err) => { //first failure triggers this });
Promise all
Promise allSettled
Promise.race() and any() - One Result
Section titled “Promise.race() and any() - One Result”Promise.race()
and Promise.any()
are very similar. They are both race conditions. They are both looking for a first Promise to be completed. They both need an array of promises to be passed in.
The difference is that race
is looking for the first one to complete, regardless whether it is resolve
or reject
, and any
is looking for the first successful result. The any
approach will
only run the catch
if all of the promises in the array are rejected.
let p1 = new Promise((resolve, reject) => { reject(1);});let p2 = new Promise((resolve, reject) => { resolve(2);});//race gives us the first result back... good or badPromise.race([p1, p2]) .then((response) => { console.log('First one back was successful'); }) .catch((err) => { console.log('First one back was rejected'); });
//any only runs the catch if all the promises failedPromise.any([p1, p2]) .then((response) => { console.log('First successful result is back'); }) .catch((err) => { console.log('no successful results'); });
Promise race
Promise any
Error Handling with Promises
Section titled “Error Handling with Promises”When you have a Promise followed by chain of then
methods an error could occur at any point in that chain of methods. If it does, then the error will be passed to the catch
at the end of the chain.
let p = new Promise((resolve, reject) => resolve(42)); //resolve immediately
let res = p .then((result) => { return result; //pass a value to then next then() }) .then((result) => { throw new Error('A Cool Error'); //this will trigger the catch() return result; //pass a value to the next then() this doesn't happen cuz of the error }) .then((result) => { return result; //final one would pass the value to `res` this doesn't happen }) .catch((err) => { //error object ends up here in `err` });//the variable `res` will still be a `pending Promise` at this point in your code//then() is an async method
With the async
await
keywords the function pauses and waits for a promise to finish resolving or rejecting. This includes any then
.
function async doSomething(){ let result = await someFunctionThatReturnsAPromise().then(result=>{ return result; //pass to then next then() }).then(result=>{ return result; //actually pass the result to `result` }); //now because of the await, `result` will have the return value from the 2nd then() console.log(result);}
You can also use a standard try catch
control-flow structure to handle errors coming back from functions.
try { doSomething(); //if an error happens in that function, the catch is triggered doSomethingElse(); //if an error happens in that function, the catch is triggered throw new Error('Sample Error'); //this will make the catch run too.} catch (err) { //this runs if an error happens in the try block}
So, if you had a call to a function that threw an error you can handle it with the catch
part of a try catch
block. It works just like the catch()
method from a Promise.then().then().catch()
chain.
Promise Error Handling
Try catch error handling
Error First Callback Pattern
The JavaScript Event Loop
Section titled “The JavaScript Event Loop”The JavaScript engines, like Chrome’s V8, use a single thread to process all your code. It builds a stack of commands to run, in the order that you have written them. As long as each command belongs to the CORE JavaScript then it gets added to the main run stack.
When something that belongs to the Web APIs is encountered it gets added to a secondary list of tasks. Each time the main stack is finished then the JavaScript engine turns it attention to the secondary list of tasks and will try to run those.
The video below does a great job explaining all the parts of this process. It is about 25 minutes so give yourself some time to watch it.
You don’t need this information to do any of the assignments this semester. However, it will help you to avoid errors in your code and to better understand how your code will be interpreted by the JavaScript engine. This will lead to you writing much better code in the long run.
Everything you need to know about the JavaScript Event Loop
And more about the event loop, asynchronous features, timers, tasks, and microtasks…
Jake Archibald: In The Loop - setTimeout, micro tasks, requestAnimationFrame, requestIdleCallback
The Event Loop you can think of as a thread. JavaScript is called a single threaded language. There is one global thread that we call the execution stack. Nearly all of our code runs on this one thread, this one event loop.
There is also a separate event loop / thread for each of our workers, which we will discuss next semester.
Tasks, MicroTasks, UI Render Tasks, and Execution Context
Section titled “Tasks, MicroTasks, UI Render Tasks, and Execution Context”A task is anything which is scheduled to be run by the standard mechanisms such as initially starting to run a program, an event being dispatched asynchronously, or an interval or timeout being fired. These all get scheduled on the task queue.
A microtask is created with queueMicrotask()
, by resolving or rejecting a Promise
, or with changes being reported by a MutationObserver
. The queued microtasks run after the queue tasks on the event loop.
A UI render task can be created with requestAnimationFrame()
. These run after the queued tasks and microtasks.
Execution Context
Section titled “Execution Context”When we run lines of JavaScript there needs to be an execution context. The execution context has a scope so the code has access to certain values and objects and create ones of its own. There are three types of execution contexts.
- The global context (
window
orself
for workers). - Functions have their own execution context.
- The
eval()
method, which should be avoided for security reasons.
There can be additional sub-scopes inside the global and function execution contexts.
Each execution context will also have its own memory spaces - heap and stack. When you create a function it will have access to it’s parent scope, which is usually the global scope, and it will also have its own scope. The function will have an execution context. Parameters that get passed to the function will have their values and their object references added to the function execution context’s stack.
The following example is a simple script with comments explaining where and when all the values, references, and execution contexts are created.
const pie = 3.14; //saved in global exec. context stackconst myobj = { id: 123, name: 'Sam',};//saved in global exec. context heap//reference called myobj saved in global stack
function hi(num, objref) { //new execution context for function hi //hi is saved in global heap //reference to hi is saved in global stack //in its stack will be num and reference to objref //objref points to myobj in global exec. context heap const four = 4; let answer = num * four + objref.id; // 3.14 * 4 + 123 = 135.64 return answer;}
hi(pie, myobj);//from the global stack, get reference to hi()//create the execution context for hi()//pass a copy of the value of pie to hi() context//pass a reference to myobj to hi() context
const two = 2; //in global exec. context stacklet val; //in global exec. context stackval = two * pie; //
On the first pass through the global context pie
, myobj
, hi
, two
, and val
are created in the global stack. On the second pass, a value will be put into pie
, and references will be put into myobj
and hi
on the stack. The actual values of myobj
and hi
will be put into the global context heap.
Then the call to hi()
will be added to the event loop stack for execution. When the event loop runs hi()
, the new execution context will be created. The value and reference for pie
and myobj
will be passed to hi
’s execution context. On the first pass for this new execution context, four
and answer
will be added to hi
’s stack. Then 4
will be set as the value for four
and the value for answer
will be calculated. These last two things will each be a command (task) added to the event loop for execution.
After hi()
has run THEN the value 2
will be assigned on the global stack to two
, as a task on the event loop. Then, finally, a task to calculate two * pie
will be added to the event loop and run.
When we trying to solve odd behaviours in our code where variables don’t seem to have the correct value and a certain point in our code, keep this sequence in mind.
Sequence of tasks, microtasks, and ui render tasks
Section titled “Sequence of tasks, microtasks, and ui render tasks”Just as was explained in the Jake Archibald video In the Loop, there are different types of tasks that happen at different points on the event loop. Understanding this sequence will help you to avoid and solve strange errors in your code.
Understanding the order of tasks and microtasks
The main stack is all the code that runs on the event loop. JavaScript wants to run all the commands in the main execution context before it does anything with the tasks or microtasks.
The UI render tasks are the one exception to leaving the event loop. The browser can decide to exit the loop and run updates (calculate and paint) for the UI. UI render tasks are mainly automatic things like updating the interface based on a CSS transition or animation. However, you can create code that runs during this phase with requestAnimationFrame()
.
request animation frame
If you create a task
and inside that task you add another task
to the event loop task queue, the second one will not run until the first task is completed and the event loop has looped around again. An example of this would be having a function on the task queue, and this function adds setTimeout
with a timeout value of zero on the task queue. Even though the time delay is zero, the event loop still has to loop around again through the microtasks and ui render tasks before it can execute the timeout callback function.
If you create a microtask
and inside that you add another microtask
to the event loop microtask queue, then the browser will also run the subsequent microtask(s) before returning to the event loop. So, a resolved Promise, that uses queueMicrotask
or Promise.resolve
will immediately run those new microtasks before allowing the event loop to continue.
Timers
Section titled “Timers”In JavaScript we can set timers to call a function at some point in the future.
We use the setTimeout()
or setInterval
functions and pass them a callback function plus a minimum time delay to use (written in milliseconds). We say minimum time delay because the main stack could still have code in it that is being run. The function from the timeout has to wait for that other code to finish before it can be called.
function f1() { console.log('f1 is running');}function f2(name) { console.log(`Hello ${name}. f2 is running.`);}
setTimeout(f1, 1000);setTimeout(f2, 1000, 'Dean'); //Dean gets passed to f2 as the argument
Timeouts
Section titled “Timeouts”When you want to run a function after a specific time delay then setTimeout is the built-in function to accomplish this.
setTimeout(myFunc, 3000); //call the function myFunc after at least 3000ms
setTimeout(function () { alert('hi');}, 2500); //run the anonymous function after 2500ms
setTimeout('myFunc()', 1000); //call the function myFunc after 1 second
window.setTimeout(myFunc, 3000); //same as first example
Intervals
Section titled “Intervals”When you want to run something repeatedly after a set time, for example, once every ten seconds, then setInterval is the function to call.
Timers running on intervals can be stopped if you call the clearInterval( ) method. Just make sure that you keep a reference to the interval.
let intervalId = setInterval( myFunc, 3000);//keep calling myFunc every 3 seconds....//use the intervalId to stop the interval calls runningclearInterval( intervalId );
Recursive Timeouts
Section titled “Recursive Timeouts”Another way to approach timed intervals is with a recursive call to the setTimeout function. Inside the function that runs following your setTimeout, you make another call to the same function. The process repeats itself.
The difference with the recursive calls is that it allows us to change the amount of time between the calls or stop it after any call.
We can use this method to create ever shorter time spans between callbacks or random times between callbacks.
setTimeout and setInterval
Async - Await for Promises
Section titled “Async - Await for Promises”One of the features added in ES6 was async
and await
. The combination of these keywords lets us use asynchronous features like fetch
inside a function but write the code in a synchronous style.
Start by making a function into an async
one.
//give a function the ability to pause and wait for a promiseasync function f1() { //this function declaration can now pause and wait for a promise to complete}
let f2 = async function () { //this function expression can pause and wait for a promise to complete};
let res = async () => { //this arrow function can also pause and wait};
Then you can use the await
keyword as many times as you want. Each one is capable of pausing the function to wait for an async result from a promise.
//make a function that can be called to create a delayfunction delay(len) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(true); //call the resolve method after `len` milliseconds have passed }, len); });}
//function ready to be pausedlet res = async () => { //this first call will run 1 second in the future let isReady = delay(1000); console.log(isReady); // `pending Promise` at this point. Not resolved yet.
//try again with await //now pause the function and wait for a result let isReadyNow = await delay(1000); //function actually pauses to wait for the result from delay() console.log(isReadyNow); //`true` //isReadyNow actually has a value this time because of await.};
Starting next week, we will be making calls to remote APIs and retrieving dynamic data. At that point we can use async await
to pause a function and wait for the result if we want.
Intro to async and await
/>