Javascript Promise 101

December 14, 2019

Knowing how Promise works in javascript will boost your development skill exponentially. Here I will share:

  1. The basic of Promise
  2. How to use then
  3. catch and error handling

I promise you this will not be as hard as you think! 🤓

What is a Promise?

Per MDN:

The Promise object represents the eventual completion (or failure) of an asynchronous operation, and its resulting value.

In beginner's term, a Promise a JS object. It doesn't have a value the moment it is declared, but it will at some time in the future. This value is either resolved or rejected.

Suppose you ordered a new hoodie from dev.to. When you purchased it, it is technically yours, but it's not quite yours because you don't have it physically, just a promise that it will be delivered. At any moment, the hoodie's status is either: arrived, on delivery, or lost. Note that even when the hoodie arrived, you may decide to reject it if it is not the right size/ you got a different product instead.

Just like the hoodie, Promise has 3 states at any time: fulfilled, rejected, pending.

Using new Promise

Let's get started using promise!

let foo = new Promise((resolve, reject) => {resolve('foo')})
foo.then(value => console.log(value) // foo

We can "shorthand" it by using Promise.resolve. Below is equivalent to above:

let foo = Promise.resolve('foo')
foo.then(value => console.log(value)) // foo

Promise + setTimeout

Let's add timeout to mimic async:

let promise1 = new Promise((resolve, reject) => {
  setTimeout(function() {
    resolve('foo');
  }, 2000)
})
promise1.then(val => console.log(val)) 
console.log("I promise I'll be first!")
// I promise I'll be first!
// ... 2 secs later  ¯\_(ツ)_/¯
// foo

Note the order of logs.

Some notes:

  1. Once promise is declared (new Promise(...)), time starts ticking.
  2. promise1 itself is a promise object. You can see it on console: promise1 // Promise {<resolved>: "foo"}
  3. You can access "foo" using then (or other async methods, but that's another article). My point is, you can't just console.log(promise1) in global scope and expect to access string "foo". You need to put console.log() inside then.

Continuous chaining

Promise can be chained, allowing you to make serial promises.

let hello1 = new Promise(resolve => resolve("hello1"))

hello1.then(val1 => {
  console.log(val1);
  return "hello2"
}).then(val2 => {
  console.log(val2);
  return "hello3"
}).then(val3 => {
  console.log(val3)
})
// hello1
// hello2
// hello3

Here you'll notice that after my hello1's then, I return "hello2". This "hello2" is the value of val2. The same with the second then, it returns "hello3" and it is the value of val3. Note that to pass on argument in promise chain, the previous then must have a return value. If you don't return value, the next then will have no argument.

Here is what I mean:

hello1.then(val1 => {
  console.log(val1);
  return "hello2"
}).then(val2 => {
  console.log(val2); // no return
}).then(val3 => { 
  console.log(val3); // val3 is undefined
})
// hello1, hello2, undefined

Chain continues, but val3 has no value because the previous chain fails to provide return value.

API call

I will only briefly touch making API call with Promise because the concept is the similar with setTimeout. Let's use fetch because it is built-in (and you can play with it on chrome console!). This code from typicode site:

let fetchTodo = fetch('https://jsonplaceholder.typicode.com/todos/1')

fetchTodo // Promise {<pending>}

fetchTodo
  .then(response => response.json())
  .then(json => console.log(json))

When we first make API call with fetchTodo = fetch('https://jsonplaceholder.typicode.com/todos/1'), it returns a Promise.

We now how to deal with promise object - just then it!

Catching Error and Rejection Handling

Remember the 2nd argument of new Promise? Suppose we don't like the result of our async operation. Instead of resolving it, we can reject it.

let fooReject = new Promise((resolve, reject) => {reject('foo rejected')})
fooReject // Promise {<rejected>: "error foo"}

It is really good habit to catch errors in promise. As a rule of thumb 👍:

Every time we use then from now on, always, always have a catch

let foo = new Promise((resolve, reject) => {reject('error foo')})
foo.then(value => console.log(value)).catch(err => console.log(err)) //gotta catch 'em all!
foo //error foo

What just happened?

Let's compare it if we had only put then without catch

foo = new Promise((resolve, reject) => {reject('error foo')})
foo.then(val => console.log(val))
// Promise {<rejected>: "error foo"}

Ah, on my chrome console, it is complaining because an error is uncaught. We need to catch the error. Let's catch it!

foo.then(val => console.log(val)).catch(err => console.log(err)) // error foo

Now we see a cleaner log!

Different rejection method

You may ask, "hey man, what if I have a chain:"

let promise1 = new Promise(fetchSomeApi);
promise
  .then(processApi)
  .then(fetchApi2)
  .then(processApi2)
  .catch(handleCommonError)

"and I want to do something different for processApi and let handleCommonError to handle the remaining errors?"

Luckily there is more than one way to catch error! then takes second argument.

Recall our first code above: let foo = new Promise((resolve, reject) => {resolve('foo')}). We will use reject for custom error handling.

You can do something like this:

promise
  .then(processApi)
  .then(fetchApi2, customErrorHandling)
  .then(processApi2)
  .catch(handleCommonError)

Should something go wrong during processApi, the result will go to .then(fetchApi2, CustomErrorHandling) line. When then sees that it sees an error/ reject, instead of firing fetchApi2, it fires customErrorHandling.

It is a good practice to still have catch even if you have reject callback.

More resolve, reject, catch examples

Resolved example:

let successFoo = new Promise((resolve, reject) => {resolve('foo')})
  .then(val => console.log(`I am resolved ${val}`), err => console.log(`I am rejected ${err}`))
  .catch(err => console.log("HELLO ERROR"))
// I am resolved foo

Rejected example:

let rejectFoo = new Promise((resolve, reject) => {reject('error foo')})
  .then(val => console.log(`I am resolved ${val}`), err => console.log(`I am rejected ${err}`))
  .catch(err => console.log("HELLO ERROR"))
// I am rejected error foo

Note that it never reach catch. The second argument handles this. If you want to reach catch, jus don't pass 2nd argument:

let catchFoo = new Promise((resolve, reject) => {reject('error foo')})
  .then(val => console.log(`I am resolved ${val}`)).catch(err => console.log("HELLO ERROR"))
// HELLO ERROR

And that's all folks! Clearly not everything is covered but the basic cases. My goal is not to make you Promise gurus, but good enough to get you started so you can do more fancy stuff. Hopefully it all makes sense!

There are more into Promise that is not mentioned, I would suggest looking up all(), finally(), and race(). I promise (😎), it's worth your time!

Thanks for reading, as always, please feel free let me know if you see an error/ typo/ mistakes.

Happy hacking!

Resources/ more readings