Promises and Async/Await From the Ground Up
Hi everyone.
This is Promises and async/await from the ground up.
My name is Valeri Karpov.
I primarily work on Mongoose, the the nodeJS and MongoDB ODM.
I also work, am the founder and CEO of a small dev shop based here in Miami beach called MeanIT software where we work primarily on Mongoose and mastering JS dot IO.
Previously I worked at several startups, including MongoDB, level up and most recently booster fuels.
So quick overview about this talk.
First we're going to take a look at Promises and specifically build a very basic Promise library from scratch in order to kind of build up the macro from the micro and understand async/aqait and Promises from the very base principles.
Building on that we're gonna take a look at async/await, see how asunc functions work specifically, how they relate to Promises.
And then finally, we're gonna take a look at how to determine whether your favorite libraries and frameworks support.
async/await.
So before Promises, most node and JavaScript developers used callbacks.
Callbacks were notoriously unwieldy, hard to work with.
Particularly when it came to complex chaining logic, if statements as well as error handling.
So around 2013, 2014 Promises started becoming very popular, especially in the node community.
And then with ES6 and 2015 Promises became a core part of the, the JavaScript language spec.
And then 2017 with ES 2017, JavaScript added async/await, which is a layer built on top of Promises.
So first, what is a Promise?
There are a lot of definitions of Promises out there, but I like to think of Promises as a state machine with three states Promise is either pending, fulfilled or rejected.
So pending means the operation is in progress.
Fulfilled means the Promise operation is, has succeeded and has a value and rejected means that the Promise that the Promise operation failed and threw an error.
And once a Promise is either fulfilled or rejected, it is called 'settled'.
The important part about settled Promises is a settled Promise never changes state again.
So once a Promise is fufilled, it stays fulfilled.
And once a Promise is rejected, it stays rejected.
So a Promise never goes back across this red line in this diagram.
So first let's take a look at a simplified Promise implementation.
You can find the full source code for this example at on my GitHub page, github.com/vkarpov15/simple-promise.
But let's take a look at the let's take a look at the code.
So a Promise is a class.
The constructor for a Promise takes in an executor function.
The Promise constructor calls the executor function with with two functions, `resolve` and `reject`.
The resolve function is how the is how the executor function tells the Promise that it should be fulfilled, and reject is how the the executor function can tell the Promise that it should be rejected.
And then there's a `value` property that contains either the result that the Promise was fulfilled with, or the error that the Promise rejected with.
So you see, we have helpers resolve and reject that transition the Promise to fulfilled and set the value, or transition the Promise to rejected and set the value to the error that the Promise was rejected with.
One key point about this simple example is is error handling.
So specifically the Promise constructor wraps the executor function call in a try/catch.
So if the executor function throws a synchronous error, that becomes a Promise rejection.
The important, this is very important because this is this is an instance of consolidated error handling where Promises let you handle both sync and async errors.
The Promises value property is supposed to be private.
So you'll as a user of a Promise, you're not supposed to be able to reach in and get the Promise's current value.
The way that you have to access the Promise's value is via the `then` function.
The then function takes in two parameters, an onFulfilled function and an onRejected function.
As you might imagine, the Promise library is, is responsible for calling onFulfilled when the Promise is fulfilled and onRejected when the Promise is rejected.
So the implementation in this case takes the the current value in the current state.
If the Promise is already fulfilled, it immediately calls onFulfilled.
If this Promise is currently rejected, it immediately calls onRejected.
Otherwise, it adds the it adds the onFulfilled and onRejected handlers to a chained array.
So when so when the Promise is resolved the the Promise library goes through the chained array, looks for any onFulfilled functions and calls the onFulfilled functions with the with the associated value.
And likewise, when a if the Promise is rejected the Promise library goes through the chained array, looks for any, any onRejected functions and then calls the onReject functions with the with the error that the Promise rejected with.
So the most important feature with Promises is Promise chaining.
Promise chaining basically means you can build up a sequence of of asynchronous operations executed in series by chaining .then calls.
And the thing that makes chaining possible is that the `then` function, returns a separate Promise.
So one of the neat features of Promise chaining is error handling.
So errors in the Promise chain go straight to the next onRejected handler or the next catch call.
So in this case, if either, if any of these Promises reject, if my Promise rejects, or if a returns, a Promise that rejects, or if b returns a Promise that rejects, or if c returns a Promise that rejects the Promise library will execute the catch handler immediately.
So in the happy path, my Promise, a, b and c all execute, but if there's an error Promise chain skips directly to catch.
So how does all this work?
There's two steps to this.
First is that the then function, returns a Promise.
So specifically, suppose that the then function returns a Promise P2 P2 takes the takes the state of P1.
So if you take a look at the source code, the then function, returns a Promise.
It creates a new onFulfilled and onRejected that are that are pushed onto the original Promises chained array.
So effectively.
The Promise P2 is now tied to the original Promise P1.
So this is step one for chaining.
Step two.
Important thing is that is that if you, if the onFulfilled function returns a Promise, we also need to take the state of that Promise.
And now that procedure is known as Promise assimilation, specifically, assimilation is responsible for handling if onFulfilled returns a Promise.
So if you resolve with a function that has a then function then the then the Promise handler is responsible for is responsible for calling the then function and then resolving the current Promises state with that with the Promise that you passed in.
So if we go back to the original, this means that if onFulfilled returns a Promise and we resolve with that Promise, that means that the value that the Promise that the then function returns P2, is now tied to the to the value that onFulfilled returned, call it P3.
Next, a brief aside on catch.
So catch is just a one line wrapper for the then function.
Catch is equivalent to calling then with a null first argument and an onRejected.
So there's nothing special about catch.
You can always just use dot then.
The, the benefit of using catch is that it's a good metaphor for for try/catch.
So now that we've taken a look at Promises, let's take a look at async/await and how it relates to Promises.
So async/await, there's a new type of, or a special type of JavaScript function called an async function.
You mark a function as async with the `async` keyword.
The, the key detail about async functions is that async functions always return a Promise.
Even if you say return something else you'll, you'll get a Promise back when you call the the async function.
And then there's the `await` keyword.
The await keyword can only be used in an async function.
So the await keyword, what does that do?
The await keyword is responsible for calling `then` under the hood.
So here's a here's a fun example.
Let's say you have a plain old JavaScript object that has a then function.
When you call await or when you execute await with that JavaScript object as the operand JavaScript will actually call that that object's then function.
So in this case, you'll see that JavaScript calls then with a with a native code onFulfilled function.
No no entries in the stack.
And then when you call onFulfilled, you get back the value that that you called onFulfilled with.
So from a different perspective how async/await works under the hood await on a Promise effectively unwraps the Promise.
So what await does is it calls then, and pauses the execution of the async function and waits for the Promise to either resolve or reject or rather fulfill or reject.
So this diagram shows that when you await on a Promise P the JavaScript runtime calls P dot then the onFulfilled parameter to to that then function returns a value.
So it makes it so that await P resolves to a given value and the onRejected handler triggers a triggers, an error that can be handled using try/catch.
So again why does async/await work?
Fulfilled Promises always stay fulfilled.
And rejected Promises always stay rejected.
You can imagine that if a fulfilled Promise could become rejected later async/await would be a problem.
The await operator wouldn't have a well defined result.
So now, how does how does concurrency work with async functions after all JavaScript is single threaded, right?
Two functions can't execute at the same time, but the but the key point is that await pauses the execution of a function until the Promise is settled.
So in other words an async function can start executing, get to an.
Stop executing for an indefinite period of time while the while the Promise is while the Promise is pending, and then start executing again once the once the Promise has been settled.
So that means that other functions can execute while a while an async function is executing an await.
So in this example, you'll see that even though I have a I have an infinite while loop the console dot log still prints.
So again consolidated error handling with async/await await P throws a throws an error that you can try/catch if P rejects.
So this once again, lets you handle synchronous and asynchronous errors using try/catch, and you don't even necessarily need to use, try/catch.
You can also use the Promise catch function to handle errors as well with async/await, both sync and async.
One common mistake that I've seen with with Promises that you should really avoid.
You can use you can use Promises and Promise chaining with async/await, but don't go the other way, and don't use an async function as an executor function.
The problem there is that Promises aren't designed to handle are designed to handle async executor functions.
So Promises don't treat a an error in an async function as a as a Promise rejection.
In other words, if your executor function returns a Promise the Promise constructor won't look for that Promise rejection.
So in this example you have, you'll end up with an unhandled error because his, the executor paused execution for some time and then threw an error that ends up as an unhandled error because because the Promise library isn't looking for for Promise rejections.
So again, in in short, some of the benefits of async/await that that make me really bullish on it.
Number one is vastly superior readability again callbacks made it hard for for outside developers or developers that were n'tcomfortable with JavaScript to to contribute to node code, code bases.
In my experience async/await makes it much easier to for non JavaScript experts to contribute to your code base, simply because you can execute async logic using four loops, if statements, try/catch, the kind of the kind of stuff that you learn in CS 101 or on your first week of bootcamp, as opposed to some more sophisticated logic or as opposed to using some sort of async library.
Another benefit is error handling async errors bubble up in, or errors in async functions bubble up in much the same way that synchronous errors do.
If you have an async function that throws an error three three levels down the error bubbles up, and you can try/catch it.
Three or four function calls up and just the same as a synchronous error.
And then finally async/await is a core part of the JavaScript language spec.
So so TC39 is committed to not breaking the internet.
And so if your code is built on async/await, your code is much more future proof.
The ... the language spec, isn't going to change on you and break your break your async/await logic.
So next, let's take a look at determining whether a whether a given library or framework supports async/await.
With libraries, the the answer is usually pretty easy.
The question is if the library does something synchronous, then you don't care.
If the library does something async, the question is, does the library return a ... return Promises rather than asking you to supply a callback?
So many common libraries in JavaScript these days support support, basic await.
They all return Promises.
For example, Axios, HTTP client, fetch everything everything returns a Promise.
So not much to worry about there.
Frameworks are a little bit trickier because again, async functions are just a special type of JavaScript function.
Their typeof is function.
You can call them.
It's fine.
But the problem is that they return Promises and not all libraries support support async functions and not all libraries are specifically looking for for handling Promise rejections.
So often frameworks don't handle async errors or don't block in waiting for the the Promise to resolve.
So for example, React generally does not support async/await, for example, render functions.
If you're using component based if you're using class based components, or if you're just returning a function with hooks that render function must be synchronous.
If you're using hooks, you can technically use async functions with useEffect and get away with it, but it's not recommended and you don't get good error handling there.
Vue generally has better support for async/await in my experience most lifecycle hooks can be async and the the good thing about it or what makes it really excellent is that errors bubble up to the errorCaptured hook.
So Vue has a neat errorCaptured hook where a component, where view calls a component's errorCaptured hook, if any, sub-component throws any sort of error.
And that does include async errors.
So if you have an async mounted hook, like two components down the tree that throws an error, that bubbles up to the parent component's errorCaptured hook.
The caveat is that async hooks don't block render.
Specifically render continues if you have an async mounted function.
So if you have a component that that has an async mounted function that goes and fetches some data, the component will render before the before the data is loaded.
So you need to be aware of that.
As of as of this talk Express the the popular Web framework the proper framework for nodeJS generally does not support async/await.
The problem is that it doesn't handle errors.
So if you write an async middleware or an async route handler that throws an error express will never return a response.
The the HTTP request will hang forever.
Express 5 does support, async/await specifically, they're making it so that so that errors or async errors or functions that return a Promise that rejects, that gets tr ...that gets handled as an error for for Express's normal error handling middleware.
So wrapping up.
Promises are just state machines.
They have some extra logic to support Promise chaining and Promise chaining means that Promise state changes propagate down the chain from one Promise to another.
And the Promises or the the async operations execute in series.
Async/await, specifically the await keyword unwraps Promises.
So that means that if you have a Promise, you wanna get the value out await is the easiest way to do it.
Async/await is a great, great way to write your to write your applications because it's great for error handling, readability and future proofing.
And that's about that's about all I have.
Further reading check out masteringjs.io.
We've got a lot of great free JavaScript content and check out my ebook Mastering async/await, which contains a lot of this information and a whole lot more.
So thank you.
Thank you very much for your time.
Here's a very cute picture of a mongoose and have a great day.