DEV Community

Chinmay Joshi
Chinmay Joshi

Posted on • Edited on

Explain JavaScript Promises like I am five.

I am starting to hate JavaScript. Before I blow my head with a gun, someone, please tell me how to work with JavaScript Promises.

cry

Top comments (23)

Collapse
 
kayis profile image
K

You know what a callback is, right?

Let's take the following example:

var c;
setTimeout(function() {
  if(c) c(true);
}, 1000);

function onTimeout(callback) {
  c = callback;
}

function myCallback(v) {
  if(v) console.log("Called!");
}

onTimeout(myCallback);

The onTimeout function takes a callback which will be stored for later use. The later use is the timeout that triggers after 1 second.

Problem here? When we call onTimeout too late, the timeout could be already have triggered, so we would add the callback, but nothing happens.

Solution? Promises!

How do they work? Easy, they are monads :D

Joke aside...

They are objects you can attach your callbacks to.

var p = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve(true);
  }, 1000);
});

function myCallback(v) {
  if(v) console.log("Called!");
}

p.then(myCallback)

Here we create a Promise object instead, often you get the final object from a function as return-value, so you don't have to think about how they are created, you often just have to call the then method on it and pass a callback.

Here we create one on our own, it takes a callback that receives two functions.

  • resolve has to be called when the asynchronous process finishes successfully.
  • reject has to be called when it fails.

As you may notice, this callback isn't the callback from the previous example. It's a wrapper to make a promise from a non-promise/callback function.

Anyway, promises bring you nice things:

You can call then as often as you like, the callbacks get all called when the promise succeeds.

p.then(function() {console.log("Hello");})
p.then(function() {console.log("World");})

You can call then whenever you like, the callbacks get called right away if the promise has succeeded in the past.

setTimeout(function() {
  p.then(function() {console.log("Hello");})
}, 9999);

You can chain promises with then to make asynchronous calls based on other asynchronous calls.

var fetchPromise = p.then(function(v) {
  return fetch(SOME_URL + "?value=" + v);
})

var jsonPromise = fetchPromise.then(function(response) {
  return response.json();
})

var someDataPromise = jsonPromise.then(function(json) {
  return json.someData;
});

someDataPromise.then(function(someData) {
  console.log(someData);
});

The then method returns a new promise. The fetch function also returns a promise.

If we pass a callback to then that returns a promise itself, then will return this new promise.

In our example, the first then call returns what fetch returns, because fetch returns a promise.

The json method of our response object also returns a promise, so then will return that promise.

In the third then callback, we return a value from that parsed JSON, so then creates a new promise for us that resolves to that value. This allows us to chain synchronous and asynchronous calls with then.

So this example could be written a bit shorter:

p.then(function(v) {
  return fetch(SOME_URL + "?value=" + v);
})
.then(function(response) {
  return response.json();
})
.then(function(json) {
  return json.someData;
})
.then(function(someData) {
  console.log(someData);
});

I hope this helps, if you got any questions, ask away :)

Collapse
 
marinbenc profile image
marinbenc🐧 • Edited

One little thing I'll add because I often see beginners get this wrong: (you wrote this but I still want to really emphasize this)

Don't have nested then blocks! then is a special function because it can return either a new promise or a concrete value. If you want to perform two promises sequentially, simple return the next promise from the then block.

Instead of

getUsers().then((users) => {
    getUserData(users[0]).then((user) => {
        // ...
    }
})

You can write

getUsers()
  .then((users) => return getUserData(users[0]))
  .then((user) => /* ... */)

Which is much more readable. In functional terms, then is both bind and map, depending on the return value.

Collapse
 
glebec profile image
Gabriel Lebec

There is at least one good case in which it is defensible to nest .then, and that's when you want to some shared reference in both callbacks.

doAsyncThing()
.then(result1 => doMoreAsync(result1.path))
.then(result2 => { /* wish I had access to result1 AND result2 here! */ })

There are several solutions to sharing scope between earlier and later callbacks, including:

A) Create a mutable outer-scope let variable and assign it partway down the promise chain: this is probably the most intuitive solution, but does involve some (disciplined) mutation and pollution of the outer scope.

let result1 // undefined

doAsyncThing()
.then(result1arg => {
    result1 = result1arg // store in outer scope
    return doMoreAsync(result1.path)
})
.then(result2 => {
    console.log(result1, result2)
})

B) Use a promise library with a feature that passes along state information in an implicit parameter, e.g. Bluebird and this (though that technically breaks the A+ spec); this is the least portable and most opaque way.

doAsyncThingViaBluebird()
.bind({}) // this is Bluebird#bind, not Function#bind – misleading!
.then(result1 => {
    this.result1 = result1 // store in implicit state object
    return doMoreAsyncWithBluebird(result1.path)
})
.then(result2 => {
    console.log(this.result1, result2)
})

C) use a nested promise chain, which introduces a little local callback indentation and verbosity but which does not pollute an outer scope or require any mutation:

doAsyncThing()
.then(result1 => {
    return doMoreAsync(result1.path).then(result2 => {
        console.log(result1, result2) // have access to both params in scope
    })
})

You can use this technique to pass both results down the chain by returning a container object such as an object or array:

doAsyncThing()
.then(result1 => {
    return doMoreAsync(result1.path).then(result2 => [result1, result2])
})
.then(([result1, result2]) => { // using array destructuring
    console.log(result1, result2)
})

The absolutely important thing however is to remember to return the internal promise chain, otherwise your next then will fire before the internal chain has actually settled.

doAsyncThing()
.then(result1 => {
    // INCORRECT OMISSION OF RETURN BELOW:
    doMoreAsync(result1.path).then(result2 => doStuffWith(result1, result2))
})
.then(() => {
    // this callback fires prematurely!
})

In practice, I more often use some outer-scope let binding over nested promise chains, but the latter are perfectly valid if done correctly.

Collapse
 
ardennl profile image
Arden de Raaij

How is this not a blogpost on itself :D

Collapse
 
joshichinmay profile image
Chinmay Joshi

Hey, thanks for explaining. This answer helps a lot.

Collapse
 
davidlecodes profile image
David Ruiz

Men, thanks for the promises information.

Collapse
 
bennypowers profile image
Benny Powers 🇮🇱🇨🇦 • Edited

Think of a regular JS array as an envelope. You put one (or more) values into the envelope, and it gives you extra behaviour like map and reduce

const a = 1
      a.map(x => x * 2) // error
      a.reduce((x, y) => x + y, 0) // error

const wrapped = [a]
      wrapped.map(x => x*2) // [2]
      wrapped.reduce((x, y) => x + y, 1) // 3

In fact, we can say that JavaScript arrays are envelopes that have the special property of including multiple values.

Promises are similar. They are envelopes just like arrays are, but the special property they have is they handle future/potential/asynchronous values.

const f = x => new Promise((res, rej) => x ? res(x) : rej(x))

      f(2)
        .then(x => x * 2)
        .then(console.log) // 4

      f(null)
        .then(x => x * 2) // this function never runs
        .catch(x => console.error(`promise rejected for ${x}`)) // promise rejected for null

This is especially useful for managing asynchronous problems. By using a functional approach, Promise flows can be written in a top-down, left-to-right manner:

const handleAsText = response => response.text()
const unsafeDocumentWrite = text => {
  document.open();
  document.write(text);
  document.close();
}

fetch('google.com') // fetch API returns a Promise<Response>
  .then(handleAsText)
  .then(unsafeDocumentWrite)
Collapse
 
joshichinmay profile image
Chinmay Joshi

I get it now...

In fact, we can say that JavaScript arrays are envelopes that have the special property of including multiple values.
Promises are similar. They are envelopes just like arrays are, but the special property they have is they handle future/potential/asynchronous values.

Thanks a lot!

Collapse
 
bennypowers profile image
Benny Powers 🇮🇱🇨🇦

Yup!

The non-eli5 answer is that Promises are (not 100% but basically) monads that encode asynchronous behaviour

Thread Thread
 
joshichinmay profile image
Chinmay Joshi

Got it. :)

Collapse
 
triplejdev profile image
Rhymes Toasterface • Edited

Like you are five? Ok....
You ask your mum if you can have some sweets later. She can either resolve sweets ( yes you can have some), or reject your request (no sweets for you. You’ve been naughty).
When it gets to later, if her promise resolved (ie she said you can have some) THEN you can eat them. Otherwise, you have to CATCH the tears you cry because you can’t eat sweets. Simple, huh?

A promise contains a resolve and reject. When it is actually called (the function is invoked), if it resolves, use THEN, or CATCH the rejection.

Collapse
 
asamolion profile image
Muhammad Osama Arshad

I've written a blog post explaining Promises. Hope it can help you in understanding.

I would also love if you could go through and point out anything that can be improved!

alazierplace.com/2018/09/promises-...

Collapse
 
joshichinmay profile image
Chinmay Joshi

Hey, thanks a lot. I will check it out. :)

Collapse
 
flaviocopes profile image
flavio ⚡️🔥

Adding my post on the subject, hoping it's useful :)

dev.to/flaviocopes/understanding-j...

Collapse
 
joshichinmay profile image
Chinmay Joshi

Thank you very much. :)

Collapse
 
isaacdlyman profile image
Isaac Lyman

Here's another thread where I answered a similar question: dev.to/shubhambattoo/explain-x-lik...

Promises can be confusing! Don't expect yourself to understand them right away.

Collapse
 
joshichinmay profile image
Chinmay Joshi

Hey, thanks for sharing.
And yes, promises are hard to adapt.

Collapse
 
kepta profile image
Kushan Joshi

Once you have graduated from promise school, you can check out some helpful tips I wrote for promises dev.to/kepta/promising-promise-tip...

Collapse
 
joshichinmay profile image
Chinmay Joshi

Hey, thank you. :)
I will look it up.

Collapse
 
ardennl profile image
Arden de Raaij

Maybe not 5 exactly, but I did the most basic explanation right here: dev.to/ardennl/about-promises-and-...

Collapse
 
joshichinmay profile image
Chinmay Joshi

Hey, thanks for sharing. :)

Collapse
 
migueloop profile image
Miguel Ruiz
Collapse
 
joshichinmay profile image
Chinmay Joshi

Thanks a lot for sharing. :) Very much helpful.