Passing data between Promise callbacks

[2017-08-08] dev, javascript, esnext, async, promises
(Ad, please don’t block)

In Promise-based code, there are usually many callbacks, each one having a separate scope for variables. What if you want to share data between those callbacks? This blog post describes techniques for doing so.

The problem  

The following code illustrates a common problem with Promise callbacks: The variable connection (line A) exists in one scope, but needs to be accessed in other scopes (line B, line C).

db.open()
.then(connection => { // (A)
  return connection.select({ name: 'Jane' });
})
.then(result => {
  // Process result
  // Use `connection` to make more queries (B)
})
···
.catch(error => {
  // handle errors
})
.finally(() => {
  connection.close(); // (C)
});

In this code, we are using Promise.prototype.finally(), which is a proposed ECMAScript feature. It works analogously to the finally clause of try statements.

Solution: side effects  

The first solution we’ll look at is to store the value to be shared, connection, in a variable in a scope surrounding all callback scopes (line A).

let connection; // (A)
db.open()
.then(conn => {
  connection = conn;
  return connection.select({ name: 'Jane' });
})
.then(result => {
  // Process result
  // Use `connection` to make more queries (B)
})
···
.catch(error => {
  // handle errors
})
.finally(() => {
  connection.close(); // (C)
});

Due to its outer location, connection is available in line B and line C.

Solution: nested scopes  

The synchronous version of the original example looks like this:

try {
  const connection = await db.open();
  const result = await connection.select({ name: 'Jane' });
  ···
} catch (error) {
  // handle errors
} finally {
  connection.close();
}

With synchronous code, the solution to making connection available in line A is usually to move the variable declaration to a surrounding scope:

const connection = await db.open();
try {
  const result = await connection.select({ name: 'Jane' });
  ···
} catch (error) {
  // handle errors
} finally {
  connection.close();
}

We can do something similar with Promises – if we nest Promise chains:

db.open() // (A)
.then(connection => { // (B)
  return connection.select({ name: 'Jane' }) // (C)
  .then(result => {
    // Process result
    // Use `connection` to make more queries
  })
  ···
  .catch(error => {
    // handle errors
  })
  .finally(() => {
    connection.close();
  });
})

There are two Promise chains:

  • The first Promise chain starts in line A. connection is the asynchronously delivered result of open().
  • The second Promise chain is nested inside the .then() callback starting in line B. It starts in line C. Note the return in line C, which ensures that both chains are eventually merged correctly.

The nesting gives all callbacks in the second chain access to connection.

You may have noticed that, in both the sync and the async code, synchronous exceptions thrown by db.open() won’t be handled by catch (clause or callback). A follow-up blog post on Promise.try() shows how to fix that for the async code. In the sync code, you can move db.open() into the try clause.

Solution: multiple return values  

The following code demonstrates another approach for passing data between callbacks. However, it doesn’t always work. In particular, you can’t use it for the ongoing database example. Let’s look at an example where it does work.

We are facing a similar problem: A Promise chain where the intermediate value intermediate should be passed from the callback starting in line A to the callback starting in line B.

return asyncFunc1()
.then(result1 => { // (A)
  const intermediate = ···;
  return asyncFunc2();
})
.then(result2 => { // (B)
  console.log(intermediate);
  ···
});

We solve this problem by using Promise.all() to pass multiple values from the first callback to the second one:

return asyncFunc1()
.then(result1 => {
  const intermediate = ···;
  return Promise.all([asyncFunc2(), intermediate]); // (A)
})
.then(([result2, intermediate]) => {
  console.log(intermediate);
  ···
});

Note that returning an Array in line A does not work, because the .then() callback would receive an Array with a Promise and a normal value. Promise.all() uses Promise.resolve() to ensure that all Array elements are Promises and fulfills its result with an Array of their fulfillment values (if none of the Promises is rejected).

One limitation of this approach is that you can’t pass data into the callbacks of .catch() and .finally().

Lastly, this technique would benefit from a version of Promise.all() that worked with objects (not just Arrays). Then you could label each of the multiple return values.

Further reading