DEV Community

Discussion on: Explain JavaScript Promises like I am five.

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.