Simple Patterns for Typescript Promises

This post explains simple patterns for using Typescript promises. These basic patterns will be sufficient to cover most standard use cases. Even though they may not be the most ‘elegant’ solutions, they are easy to understand.

The trigger for this post is a problem we had recently with our Mocha tests: They were instable, because the after hook was not waiting for tidy-up work to finish. Since the methods do not return a value, the problem went un-notized for a long time.

This was a good reason to refresh my memory on promises. What did I find? Lots of very good posts explaining promises, chaining them and more. The solution to our problem was really simple in the end. For this reason here the easiest patterns I could find on how to use promises.

Simple Patterns for Typescript Promises

Warning: Antipattern!

The code below may look like whether something is waiting there for anybody who is not very familiar with promises in Typescript. But beware – in foreach loops any returned promises are just thrown away! The work happens and will finish eventually.

The following code snippet is taken from a set of mocha tests, if you want to have a play have a look at the test in github.

async function slowCall(name: string, timeoutMs: number): Promise<string> {
  console.log(`Level ${name} started, timeout ${timeoutMs}`);
  await new Promise((resolve) => setTimeout(resolve, timeoutMs));
  console.log(`Level ${name} finished`);
  return Promise.resolve(`${name}(${timeoutMs})`);
}

[...]
//some mocha test
console.log('test entry');
const callsToMake = [
{ name: '1 - Array.foreach', ms: 200 },
{ name: '2 - Array.foreach', ms: 150 },
];
callsToMake.forEach(async (call) => {
console.log('Result: ', await slowCall(call.name, call.ms));
});
console.log('test exit');

Below the output when running the test suite – the ‘slowCalls’ started in the first test finish about 2 tests later. In our specific case this was causing intermittent failing tests due to deadlock exceptions.

Simple Patterns for Typescript Promises

Following some simple patterns for typescript promises which will wait!

1. Standard: await of One Promise

The most basic use of the await keyword is to wait for exactly one promise.

try {
await asyncMethod(); // returns a Promise<SomeType>
} catch (error) {
   // error handling
}

Note:

  1. Without the await keyword no exceptions will ever be caught.
  2. If no error handling is required, the try-catch block can be removed.
  3. For methods which are not async and return a value instead of a promise, the code will still work and use the returned value.

2. Call an async Function per Item in a List – Sequential

A potential use case for Promises is to call an async function for each item in a list. A for-of loop will iterate through all items in a list and wait for any promises to be resolved.

Note: Calling the same async function for each item in a list is an indication for a potential performance problem and should be reviewed! However, in some cases there may not be an easy way around this pattern, for instance when a third party API we cannot change only accepts single IDs as parameter.

const ids = [ 1, 2];
for (const id of ids) {
 await deleteItem(id);
}

3. Run Promises Parallel and Wait for All

Use Promise.all to make multiple async calls in parallel and retrieve the results for all of them.

const allPromises = [slowCall('1', 100), slowCall('2', 50)];
const results = await Promise.all(allPromises);
console.log('All promises are resolved');
results.forEach((result) => console.log('Result: ', result));

Note:

  1. If any of the promises throws an error, then Promise.all throws an error and does not return any results at all.
  2. Running calls in parallel which query/change the same resource may lead to concurrency or deadlock exceptions.

4. Wait for the First of Multiple Promises

In some cases only the first of multiple responses is needed. In this case Promise.race returns only the result of the promise which finishes first.

// slowCall returns a promise which is stored in the list
const allPromises = [slowCall('1', 100), slowCall('2', 50)];
const result = await Promise.race(allPromises);

Note:

  1. All calls will finish, but only the result of the first call is returned – either the result or an error is thrown.
  2. If any of the calls finishing later throws an error, the error will ‘disappear’.

Conclusion

Looking at these code samples makes everything seem to easy and logical. How could we miss this?

The tests in a little sample project can be found in GitHub: https://github.com/AngelaE/ts-play/tree/v0.1/tests

Angela Evans

Senior Software Engineer at Diligent in Christchurch, New Zealand