The Ultimate JavaScript Promise Tutorial


One of the features that make JavaScript stands out from other high-level programming languages is its asynchronicity. JavaScript makes us very easy to run multiple tasks without blocking each other.

Traditionally, this thing can be achieved in other programming languages by using “threading”. In Python, for instance, we can run a separate thread to do some heavy tasks without blocking the main thread, and get notified when the job is finished. But since JavaScript is “non-blocking” by its nature, we don’t need to do such things. Instead, we can use something called Promise.

What is Promise?

In a nutshell, Promise is an object in JavaScript that may produce a value some time in the future.

One of the most common use cases of Promise is fetching data from an API. It happens a lot of times, especially in dynamic web applications. Take a look at this example.

console.log(fetch("https://jsonplaceholder.com/posts/1"))

Here, we’re using the JavaScript Fetch API to fetch a fake blog post from JSONPlaceholder. If you run this code on your browser, here is what you’ll get.

Promise { <state>: "pending" }

When we’re trying to fetch data from somewhere else on the internet. We don’t get the data eventually, because it really depends on the user’s internet connection.

Even though we don’t know exactly when the data will arrive, we can add a then handler to our promise so that we can do something about it once it has arrived.

fetch("https://jsonplaceholder.typicode.com/posts/1").then((response) => {
	console.log("status:", response.statusText)
})

If you run it, the result should look like this.

status: OK

Handling Errors

In JavaScript, Promise has three states, pending, rejected, and fulfilled.

The pending state happens right after we create a new promise. This can take some time depending on the task that the promise is running. Once the task is done, it will change to either fulfilled or rejected.

Let’s go back to the fetch example.

fetch("https://jsonplaceholder.typicode.com/posts/1")
	.then((response) => {
		console.log(response.ok)
	})
	.catch((err) => {
		console.error("Failed to fetch post!")
	})

In some circumstances, fetching data over the internet can fail. Things like the interrupted internet connection or the unexpected server error might happen to our users.

We can add a function to handle those errors by ourselves by adding a catch method to our promise. Just like then, catch method expects a function as a parameter which will be triggered when bad things happened during the request.

Creating Promise

Most of the time, you don’t need to create a Promise by yourself, since the JavaScript APIs and third-party libraries already provide the Promise for you. But, you still can make your own Promise using the Promise constructor.

const myPromise = new Promise((resolve, reject) => {
	resolve("Success!")
})

myPromise.then((data) => console.log(data))

A Promise object requires a callback function.

The callback function gets two parameters. The first one is to resolve the promise so that it executes the given then handler. The second one, however, is to reject the promise so that it goes to the catch handler.

Both functions can accept a value that will be given to the handler.

Success!

Chaining Promises

Sometimes, you want to wait until an asynchronous operation is finished before you jump into another async code. This happens a lot when you’re trying to fetch JSON data using fetch.

Take a look at this example:

fetch("https://jsonplaceholder.typicode.com/posts/1").then((response) => {
	response.json().then((data) => {
		console.log(data)
	})
})

Here, we’re trying to fetch JSON data from an API, and once we get the raw response, we want to transform it into an object. Because both of these operations are asynchronous, we need to wait until we get the response before we can transform it.

This code works well, but it doesn’t look good. However, if you just return the response.json result in the callback and add another then method next to that, you’ll get the same result.

fetch("https://jsonplaceholder.typicode.com/posts/1")
	.then((response) => response.json())
	.then((data) => {
		console.log(data)
	})

Now, we get the exact same result but with much cleaner code.

Object {
  userId: 1,
  id: 1,
  title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}

Async & Await

Most programming languages have async and await in their syntax. Basically, they are just an alternate syntax to handle asynchronous code like Promises to make it looks cleaner and readable. Let me give you an example.

fetch("https://jsonplaceholder.typicode.com/posts/1")
	.then((response) => {
		console.log(response.ok)
	})
	.catch((err) => {
		console.error("Failed to fetch post!")
	})

When using the traditional then and catch method, we’re forced to wrap our code inside callbacks which makes our code.

Remember this piece of code? Well, this function looks okay, but we actually can improve this code using async and await syntax to flatten the code hierarchy.

async function fetchPosts() {
	const response = await fetch("https://jsonplaceholder.typicode.com/posts/1")
	const data = await response.json()
	console.log(data)
}

To catch the errors, you can wrap all asynchronous operations you want to catch inside a try block. What’s cool about this is that you can handle the errors of multiple promises in a single code block.

async function fetchPosts() {
	try {
		const response = await fetch("https://jsonplaceholder.typicode.com/posts/1")
		const data = await response.json()
		console.log(data)
	} catch (err) {
		console.error("Failed to fetch post!")
	}
}

Tags: Javascript