scattering clouds

Coding practice 6: JavaScript Promises and async/await.

Lately, I have been poking around with headless CMSes. I wanted to learn how to interact with them programmatically using purely vanilla JavaScript (JS).

In order to do that, I need to write code that connects to a headless CMS to create, read, update, and/or delete information, which can be done using the JS Fetch API.

The Fetch API makes use of a very interesting JS concept known as a “Promise”. So the first thing I need to learn is what Promises are, and how they actually work.

JavaScript Promises work similarly to promises in real life.

Let’s say your mom is baking a cake, and asked you to run to a grocery store nearby for some eggs. She gave you 10 dollars, and you gave her a promise: to return with some eggs, preferably a carton of 12 eggs… and some change for your troubles.

Mom wants you to hurry, but it is uncertain when you would return from the grocery store, and whether you can actually return with exactly 12 eggs in tow.

  • There might be a very long queue at the checkout counter.
  • The store might have redecorated and it took a while to find some eggs.
  • The store might be out of cage-laid eggs, and you only have enough money to buy a carton of 6 eggs of the organic, free-range variety.
  • Or the store might be out of eggs altogether.
  • You might even drop the carton and break a few eggs on your way home!

The definition of a JavaScript Promise.

Following the analogy above, a JS Promise is an object that represents the eventual completion (or failure) of a task, and its resulting outcome.

The Promise object is a proxy for a result that is not necessarily known when it is created: instead of producing an immediate result, the Promise will supply a result or outcome at some unknown point in the future.

A Promise can be in one of three states.

  • Pending: the Promise is in its initial state and its results are undefined.
  • Fulfilled: the task or operation pertaining to the Promise has completed successfully, and its results are available for further evaluation or use.
  • Rejected: the task or operation has failed and the result is an error object.

The constructor for a JS Promise looks a little something like this:

let promise = new Promise(function(resolve, reject){

	// some code that takes a while to produce a result
	let runSomeCode = function(){
		let outcome = perform_some_slow_or_complex_computation();
		
		// invoke either resolve or reject based on the outcome
		if(isSuccessful(outcome)) resolve(outcome.result);
		else reject(outcome.reason);
	};
	
	runSomeCode();
});

The Promise constructor takes a function as an argument – this function is known as the executor(). The executor() function will run automatically when the Promise is created, and the function itself will take two callback functions resolve() and reject() as its arguments.

Once the executor function has produced a result, it will pass the result on to one of the callback functions:

  • If the outcome is successful, the executor will invoke the resolve() callback function, and pass on the result. The Promise is now Fulfilled.
  • If the outcome is not successful, the executor will invoke the reject() callback function and pass on the reason it failed. The Promise is now Rejected.

There can only be a single possible outcome for each Promise, and any state change is final.

  • The executor should invoke only one resolve() OR one reject(). All further calls to resolve() and reject() will be ignored.
  • If an error is thrown in the executor(), the Promise becomes Rejected, unless resolve() or reject() has already been invoked.
  • The return value of the executor() function has no impact on the Promise. If executor() exits without ever invoking either one of its callback functions, the Promise will remain Pending forever, with no definitive result or outcome.

Handling the result of a settled Promise.

A Promise is said to be settled if it is either Fulfilled or Rejected. Once a Promise is settled, we can use / handle its result using the following methods:

  • .then()
  • .catch()
  • .finally()

The .then() method.

The syntax of the .then() method is as follows:

let promise = new Promise(function(resolve, reject){
	// some code that takes a while to produce a result
	// the code will invoke either resolve() or reject() based on the outcome
});

promise.then(
	function(result){ /* Process the result of a Fulfilled Promise */ },
	function(reason){ /* Process the reason of a Rejected Promise */ }
);

This method takes up to two callback functions as its arguments.

  • The first callback function runs and performs some computation on the result received from resolve() when the Promise is Fulfilled.
  • The (optional) second callback function runs when the Promise is Rejected and acts upon the reason received from reject().

The .catch() method.

If we are only interested in handling Rejections in the event of an unfavourable outcome, or when an error is thrown in the executor(), we use .catch():

let promise = new Promise(function(resolve, reject){
	// some code that takes a while to produce a result
	// the code will invoke either resolve() or reject() based on the outcome
});

promise.catch(
	function(reason){ /* Process the reason of a Rejected Promise */ },
);

.catch() is simply a shorthand – it is essentially the same as using undefined or null for the first argument of .then():

promise.then(
	null,
	function(reason){ /* Process the reason of a Rejected Promise */ }
);

The .finally() method.

The .finally() method will always run when the Promise is settled – regardless of whether it is Fulfilled or Rejected.

let promise = new Promise(function(resolve, reject){
	// some code that takes a while to produce a result
	// the code will invoke either resolve() or reject() based on the outcome
});

promise.finally(
	function(){ /* Always invoke this function when the Promise is settled */ },
);

The callback function passed to .finally() does not take any arguments, because it does not matter whether the Promise is Fulfilled or Rejected. As long as one of resolve() or reject() is invoked, the callback function will run regardless.

It is also important to note that .finally() will pass on the result or reason of the settled Promise to the next suitable handler, allowing you to tag on additional methods to handle the outcome in a chain of Promises.

Promise chaining.

.then(), .catch(), and .finally() are used to associate further action with a settled Promise. Since these methods all return Promises, they can be chained together:

let promise = new Promise(function(resolve, reject){
	// some code that takes a while to produce a result
	// the code will invoke either resolve or reject based on the outcome
});

promise.then(
	onResolvedA,
	onRejectedA
).then(
	onResolvedB,
	onRejectedB
).then(
	onResolvedC,
	onRejectedC
).catch(
	onRejectedD
).finally(
	alwaysRunThisFunctionRegardless
).then(
	onResolvedE,
	onRejectedE
);

Promise.resolve() and Promise.reject().

The syntax of Promise.resolve() and Promise.reject() are quite straightforward:

let resolvedPromise = Promise.resolve(value);

let rejectedPromise = Promise.reject(reason);

These are both static methods. The former resolves a given value to a Promise, and the latter returns a Promise that is Rejected with the given reason.

Return value of .finally().

We mentioned previously that .finally() will pass on the result or reason of the settled Promise to the next handler in a Promise chain. This is only partially true.

The return value of the callback function in .finally() will be ignored UNLESS

  • the returned value is a Rejected Promise, OR
  • an error is thrown in the callback function,

in which case .finally() will return a Promise that is Rejected with that value or error.

let promise = new Promise(function(resolve, reject){
	resolve('Success!');
});

promise.finally(
	() => Promise.reject('Error!')
).then(
	result => console.log(`Resolved with the message: ${result}`),
	reason => console.log(`Rejected with the reason: ${reason}`)
);

As an example, the output of the above code is “Rejected with the reason: Error!”. If the callback function in .finally() returned anything else, the output of the code will become “Resolved with the message: Success!”.

Return value of .catch() and .then().

.catch() is simply a shortcut of .then(), so we only need to learn about .then().

Once a Promise is settled, .then() will perform a callback depending on whether the Promise is Fulfilled or Settled, and then it would return a Promise. How the returned Promise (rP) behaves will depend on the execution result of the callback function.

According to Mozilla’s documentation, if the callback function

  • returns a value, rP gets Fulfilled with the returned value as its result.
  • does not return anything, rP gets Fulfilled with undefined as its result.
  • throws an error, rP gets Rejected with the thrown error as its reason.
  • returns an already Fulfilled Promise, rP gets Fulfilled with that Promise’s result as its result.
  • returns an already Rejected Promise, rP gets Rejected with that Promise’s reason as its reason.
  • returns another Pending Promise, rP is Pending and becomes Fulfilled/Rejected with that Promise’s outcome as its outcome immediately after that Promise becomes Fulfilled/Rejected.

A working example using JavaScript Promises.

We can model our egg analogy using JS Promises:

const randomIntegerBetween = (min, max) => {
	return Math.floor(Math.random() * (max - min + 1)) + min;
};

let makePromise = new Promise( (onSuccess, onFailure) => {
	const buyEggs = (timeInMillis) => {
		setTimeout(() => {
			let successChance = Math.random();
			if(successChance < 0.7){
				let eggCount = randomIntegerBetween(1, 12);
				onSuccess(eggCount);
			} else onFailure('No eggs available in store!');
		}, timeInMillis);
	};
	buyEggs(2000 + Math.random() * 2000);
	alert('Heading out to buy some eggs! This might take a few seconds...');
});

const purchaseSuccessful = (eggCount) => {
	alert(`Brought ${eggCount} egg${eggCount > 1 ? 's':''} safely back home.`);
	return eggCount;
};

const purchaseFailed = (problem) => {
	alert(problem);
	return Promise.reject(problem);
};

const bakeCake = (eggCount) => {
	alert(`Baking ${eggCount <= 2 ? eggCount <= 1 ? 'a tiny ' : 'a small ' : ''}cake now!`);
};

const noBake = (problem) => {
	alert('Unable to bake a cake today...');
};

makePromise.then(purchaseSuccessful, purchaseFailed).then(bakeCake, noBake);

The code is kept intentionally verbose to make it easier to read. What it essentially does is create a Promise to purchase some eggs from the store.

  • It can take anywhere between 2 to 4 seconds to return from the store.
  • There is a 30% percent chance that the store is completely out of eggs.
  • The amount of eggs that is brought home safely comes from a randomly generated integer between 1 to 12.

You can click on the button below to run the code. Give it a few tries and see how easy or difficult it is to get eggs from this particular store!

JavaScript async and await.

There is a special syntax in JS that allows us to work with Promises in an easier and more convenient manner called async/await.

The JS keyword async can be placed before a function declaration like so:

async function f1(){
	// some code for function 1
}

const f2 = async function(){
	// some code for function 2
}

const f3 = async () => {
	// some code for function 3
}

The async function declaration essentially does only one simple thing: it makes the function return a Promise. And it can be utilised as such:

const myAsyncFunction = async () => {
	return 'Hello world!';
}
myAsyncFunction().then(
	(result) => alert(result)
);

myAsyncFunction() simply wraps the return value in a Fulfilled Promise, which can subsequently be handled using .then(). The above example is equivalent to:

new Promise((resolve, reject) => {
	resolve('Hello world!');
}).then(
	(result) => alert(result)
);

As we can see, the async keyword is really just a different way to create a Promise without making use of the new Promise() constructor.

What makes this syntax so useful is actually the await keyword, which can only be used inside an async function. It tells the function to pause and wait for a Promise inside the async function to become settled before continuing to run.

await is really useful to ensure that your code is running in the right sequence.

If you run the code in the following example, you will notice that the alerts would pop up out of order, with the second alert appearing before the first.

const outOfOrder = async () => {
	let slowRunningPromise = new Promise( (resolve, reject) => {
		setTimeout(
			() => resolve('This alert should appear first, but it is lagging behind.'),
			2000
		);
	});
	slowRunningPromise.then((result) => alert(result));
	alert('This alert should appear second, but it appeared first.');
}
outOfOrder();

We want our code to run in the sequence it is written in, but the code in the example above simply kept running without waiting for slowRunningPromise to resolve, causing the second alert to appear first.

This is where the await keyword comes in handy. By adding it to .then(), we tell the code to wait for slowRunningPromise to resolve first, before continuing on.

const runInOrder = async () => {
	let slowRunningPromise = new Promise( (resolve, reject) => {
		setTimeout(
			() => resolve('This alert should appear first as intended (after some waiting).'),
			2000
		);
	});
	await slowRunningPromise.then((result) => alert(result));
	alert('This alert should appear second, which is also in the correct order.');
}
runInOrder();

To reiterate, await literally tells the function to stop executing and wait for the slowRunningPromise to resolve. It is simply a more elegant way to read and write code – as opposed to chaining together multiple .then() handlers.

back to top