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.

Promises and Async/Await From The Ground Up

Valeri Karpov

About Me

  • Mongoose: Node.js MongoDB ODM
  • Formerly @ MongoDB, LevelUp, Booster
  • Now FT on Mongoose, Mastering JS, etc.

Talk Overview

  • Promises: what is a promise and why it matters
  • Async/Await: how async functions work
    • ○ And how async/await relates to promises Does It Support Async/Await?
    • ○ React, Vue, Express

On Promises and Async/Await

  • Before ES6: callbacks
  • 2015: promises
  • 2017: async/await

What is a Promise?

  • State machine representing an operation
  • Pending: in progress
  • Fulfilled:succeeded
  • Rejected:failed
  • Settled: not pending

A Simplified Promise Implementation

class Promise {
	constructor(executor) {
		this.state = 'PENDING';
		this.chained = [];
		// Not used yet
		this.value = undefined;

		try {
			// Reject if the executor throws a sync error
			executor(v =>gt; this.resolve(v), err =>gt; this.reject(err));
		} catch (err) {
			this.reject(err);
		}
	}

	// Define `resolve()`  and `reject()` to change the promise state
	resolve(value) {
		if (this.state !== 'PENDING') return;
		this.state = 'FULFILLED';
		this.value = value;
	}
	reject(value) {
		if (this.state !== 'PENDING') return;
		this.state 'REJECTED';
		this.value = value;
	}
}

Key Point: Consolidated Error Handling

  • reject() async errors
  • try/catch sync errors

Same code as in the previous slide with red arrows pointing to reject and catch.

The Promise then() Function

  • value is private
  • then() allows access
  • If settled, call handler
  • Else add to queue
then(onFulfilled, onRejected) {
	const {
		value,
		state
	} = this;

	// If promise is already settled, call the right handler
	if (state === 'FULFILLED')
		return setImmediate(onFulfilled, value);
	if (state === 'REJECTED')
		return setImmediate(onRejected, value);
	// Otherwise, store handlers so you can call them later
	this.chained.push({
		onFulfilled,
		onRejected
	});
}
resolve(value) {
	if (this.state !== 'PENDING') return;
	Object.assign(this, {
		value,
		state: 'FULFILLED'
	});
	// Loop over `chained`, find `onFulfilled()` functions.
	// Remember that `.then(null, onRejected)` is valid.
	this.chained.
	filter(obj =>gt; obj.onFulfilled instanceof Function)
		.

	// The ES6 spec section 25.4 says `onFulfilled' and
	// 'onRejected` must be called on next event loop tick
	forEach(obj =>gt; setImmediate(obj.onFulfilled, value));
}
reject(value) {
	if (this.state !== 'PENDING') return;
	Object.assign(this, {
		value,
		state: 'REJECTED'
	});
	this.chained.
	filter(obj =>gt; obj.onRejected instanceof Function).
	forEach(obj =>gt; setImmediate(obj.onRejected, value));
}

Promise Chaining

#1 Promise feature: chaining

function getWikipediaHeaders() {
	return stat('./headers').
	then(res => {
		if (res == null) {
			// If you return a promise from 'onFulfilled()', the next
			// then ( ) call's ConFulfilled()° will get called when
			// the returned promise is fulfilled...
			return get({
				host: 'www.wikipedia.org',
				port: 80
			});
		}
		return res;
	}).
	then(res => {
		// So whether the above 'onFulfilled() returns a primitive
		// or a promise, this 'onFulfilled() gets a headers object
		return writeFile('./headers', JSON.stringify(res.headers));
	}).
	then(() => console.log('Great success!')).
	catch(err => console.err(err.stack));
}

Error Handling with Promise Chaining

Errors in promise chain go straight to catch()

myPromise
	.then(() => a()) // skipped if `myPromise` rejected
	.then(() => b()) // skipped if `myPromise` or a()` rejected
	.then(() => c()) // skipped if `myPromise,`a()`, or `b()` rejected
	.catch(err => {
		// Called immediately if `myPromise`,`a()`, `b()`, or `c()` reject
	})

Assimilation in then()

  • then() returns promise P2
  • P2 “assimilates” original P1
  • P1 state changes propagate to P2
  • Step 1 for chaining
  • Step 2: resolve()
then(_onFulfilled, _onRejected) {
	// `onFulfilled' is a no-op by default...
	if (typeof _onFulfilled !== 'function') _onFulfilled = V => V;
	// and onRejected just rethrows the error by default
	if (typeof _onRejected !== 'function') {
		_onRejected = err => {
			throw err;
		};
	}

	return new Promise((resolve, reject) => {
		// Wrap onFulfilled' and onRejected for two reasons:
		// consistent async and try/catch
		const onFulfilled = res => setImmediate(() => {
			try {
				resolve(_onFulfilled(res));
			} catch (err) {
				reject(err);
			}
		});
		const onRejected = err => setImmediate(() => {
			try {
				// Note this is resolve, not reject. The then()
				// promise is fulfilled if onRejected doesn't rethrow
				resolve(_onRejected(err));
			} catch (err) {
				reject(err);
			}
		});
		const {
			state
		} = this;
		if (state ' FULFILLED'
			return onFulfilled(this.value);
			if (state === REJECTED ' return onRejected(this.value);
				this.chained.push({
					onFulfilled,
					onRejected
				});
			});
	}
}

Promise Resolution

  • Handle if onFulfilled returns a promise
  • Assimilate the promise

Photo of Captain Picard assimilated by the borg

resolve(value) {
	if (this.state !== 'PENDING') return;
	if (value this) {
		return this.reject(TypeError(`Promise resolved to itself`));
	}
	// Is value thenable? If so, fulfill/reject this promise when
	// value fulfills or rejects. The Promises/A+ spec calls this
	// "assimilating" the other promise (resistance is futile).
	const then = value.then;
	if (typeof then === 'function') {
		try {
			return then.call(value, V => this.resolve(v),
				err => this.reject(err));
		} catch (error) {
			return reject(error);
		}
	}
	// If value is **not** a thenable, transition to fulfilled
	this.state 'FULFILLED';
	this.value = value;
	this.chained.
	forEach(obj => setImmediate(obj.onFulfilled, value));
}

A Brief Aside on catch()

  • catch() is a 1-line wrapper for then()
  • catch() just registers an onRejected handler
catch (onRejected) {
	return this.then(null, onRejected)
}
// Below 2 are equivalent
promise.catch(onRejected);
promise.then(null, onRejected);

Async/Await

  • New type of function: async function
  • An async function always returns a promise
  • Async functions can use the await keyword
async function run() {
	const res await Promise.resolve('Hello, World!');
	console.log(res); // 'Hello, World!'
}
const 0 = run();
p instanceof Promise; // true

Async/Await and Promises

  • await calls then() under the hood
const p = {
	then: onFulfilled => {
		// Prints "then( ): function () { [native code] }"
		console.log('then():', onFulfilled.toString());
		// Only one entry in the stack:
		// Error
		// at Object. then (/examples/chapter3. test. js:8:21)
		console.log(new Error()
			.stack);
		onFulfilled('Hello, World!');
	}
};

console.log(await p); // Prints "Hello, World!"

Async/Await Under the Hood

  • await calls then() function
  • returns value or throws err: “unwraps” promise

Why Async/Await Works

  • Fulfilled promise stays fulfilled
  • Rejected promise stays rejected

an oval labelled 'pending with arrows pointing to two ovals to its right, one labelled 'fulfilled' and one labelled 'rejected'. A vertical red live separates 'pending' from the others

Concurrency With Async Functions

  • Two functions can’t execute at the same time
  • await pauses async function execution
  • Means other functions can run during await
run().catch(error => console.error(error.stack));

async function run() {
	//This will print, because 'run() is paused when you 'await
	setImmediate(() => console.log('Hello, World!'));
	// Each iteration of the loop pauses the function
	while (true) {
		await new Promise(resolve => setImmediate(resolve));
	}
}

Consolidated Error Handling, Revisited

  • await p throws try/catch-able error if p rejects
  • await lets you handle sync and async errors
const rejected = Promise.reject(new Error('Oops!'));

try {
	await rejected;
} catch (err) {
	console.log(err.message); // Oops!
}

Mistake to Avoid: Async Executors

  • Do not make promise executors async
  • Promises only reject on sync error or reject()
const p = new Promise(async function executor(resolve, reject)
await new Promise(resolve => setTimeout(resolve, 10));

// Throws an unhandled error
throw new Error('fail');
});

p.then(
	() => console.log(' Promise succeeded'), // never called
	() => console.log('Promise failed') // never called
);

In Summary: Why Async/Await

  • Readability: async with for, if, try/catch
  • Error handling: async errors bubble up same as
  • sync errors
  • Future proof: built in part of JS language spec

Does Library X Support Async/Await?

  • Libraries are easy: does the library return promises?
  • Axios, fetch, etc. all return promises

Does Framework X Support Async/Await?

  • Tricky: async functions are functions
  • Often frameworks don’t handle async errors or don’t block wait for promise to resolve

React and Async/Await

  • React generally does not support async/await
  • render() function must be sync
  • You can use async functions with useEffect()
  • But not recommended, no error handling

Vue and Async/Await

  • Vue generally does support async/await
  • Most lifecycle hooks can be async
  • Errors bubble up to errorCaptured hook
  • Caveat: async hooks do not block render
  • Render continues during async mounted()

Express and Async/Await

Wrapping Up

  • Promises are just state machines
  • Promise chaining means promise state
  • propagates down the chain
  • Async/await: await unwraps promises
  • Async/await is great for error handling and readability

Further Reading

Mastering JS Mastering Aync/Await: asyncawait.net

Thanks for Listening!

Photo of cute baby mongoose.