Await. And then? #4/4: we still need thenables

Photo by Patrick on Unsplash

Await. And then? #4/4: we still need thenables

Thenables cannot be deprecated by async/await, they still shine!

In previous bites, we saw in detail the two approaches to Javascript asynchronous programming: thenables and async/await. While the latter is designed to solve the issues .then has, they are not covering all the cases that thenables used to.

In my experience, I faced promises implicit dependencies only a few times and they fell under two categories: API enforcing call order and unit testing with timeouts.

API-enforced call order

The first case was when an API was enforcing to call its endpoints in a particular order, either by design or to prevent errors. In either case, this is a bad API design. In the sample below, we can unlock call1 on the server only when call2 completes, as there is a direct dependency between call2API and unlockCall1API. But we cannot reach call2API until call1API is over, this is a blocked situation!

const data1 = await call1API() // waits for unlock call
// cannot reach this code, ever!
console.log("data1", data1)
const data2 = await call2API()
unlockCall1API(data2)

When the server exposes two different APIs but forces them to be called in a particular order or everything would go nuts, we should seriously think of merging them in a single API call. This is a code smell that goes beyond the asynchronous programming world.

Unit testing with timeouts

The only other form of implicit dependency I met so far, is a loose one as does not involve two promises, but one single promise and a timeout. It was related to unit testing (not integration testing) with fake timers. It's better to show off some code for this one. Here I am using Jest timer mocks.

// better not using real timers on unit tests
jest.useFakeTimers();
jest.mock("./getBananasAPI"); // we don't want to fire any real call
test("when getBananasAPI does not reply, then it rejects after timeout",
async () => {
  const bananas = await getBananas()
  jest.runAllTimers() // never reached!
  [...]
})

Let's discuss this piece of code:

  1. we are using Jest timer mocks, as we don't want our unit tests to wait for whatever small amount of time we think is suitable. It has to be immediate (we are unit testing, after all).

  2. getBananas is not going to fire any API call, it will just wait for a mock to resolve a promise, but that won't ever be resolved. That is what we want in our test, as we need to wait for our timeout.

  3. jest.runAllTimers() call won't ever be reached as the execution is stuck at getBananas and our test will fail by test timeout.

Thenables to the rescue!

Those two examples are a problem because of await execution model is blocking. To overcome those issues, let's turn back to our old good thenables!

// API-enforced call order: fix with only thenables...
call1API() // waits for unlock call
  .then(data1 => console.log("data1", data1))
// this time our call is reached!
call2API().then(data2 => unlockCall1API(data2))

// ...or with mixed approach
call1API() // waits for unlock call
  .then(data1 => console.log("data1", data1))
// reached!
const data2 = await call2API()
unlockCall1API(data2)

Because of thenables execution model is non-blocking, we can unlock our call1API!

Let's see now how it goes for unit testing with fake timers.

// better not using real timers on unit tests
jest.useFakeTimers();
jest.mock("./getBananasAPI"); // we don't want to fire any real call
test("when getBananasAPI does not reply, then it rejects after timeout",
() => {
  getBananas().then(bananas => ... )
  jest.runAllTimers() // reached!
  [...]
})

Same problem, same fix. getBananas waits, but the execution model is non-blocking and we can run our fake timers, effectively simulating an API timeout.

Conclusions

While async/await is a must-go for the vast majority of use cases out there, I don't think we should get rid of thenables anytime soon, as they are the only way to express indirect promise dependency without getting stuck in a "deadlock-like" situation.

I hope you liked this deep delve inside JavaScript asynchronous programming!

To the next bite!

References