async/await with Promise in TypeScript

promise-in-typescript-eye-catch JavaScript/TypeScript

I think asynchronous process is one of the big issues that beginners struggle with. But this technique is very important to be an intermediate programmer and to make software responsive. JavaScript offers Promise for this purpose. Let’s learn promise and make your code faster.

Sponsored links

What is Promise

When you want to do your homework while your family cooks for dinner you can promise that he/she will notify you when it’s ready. In the meantime, you can work on your task. Let’s write a simple code for this.

// Basic.ts
const promise = new Promise((resolve) => {
    console.log("promise in")
    global.setTimeout(() => { resolve("Hey, dinner is ready."); }, 1000);
    console.log("promise out")
});
console.log("I start my homework.");
promise.then((value) => {
    console.log(value); // "Hey, dinner is ready."
    console.log("Dinner was so nice.");
});
console.log("I finished my homework.");

// promise in
// promise out
// I start my homework.
// I finished my homework.
// Hey, dinner is ready.
// Dinner was so nice.

This behavior is similar to setImmediate. Let’s see the result of setImmediate.

// Immediate.ts
console.log("1");
global.setImmediate(()=>{
    console.log("2");
});
console.log("3");

// 1
// 3
// 2

setImmediate function immediately returns NodeJs.Immediate object and the program continue to work in the current function. As a result, the order of the output was 3, 2, 1. What Promise does is basically the same but it is a little different from setImmediate. setImmediate puts the callback function onto the callback stack and then returns the object whereas Promise returns the Promise object when it reaches the end line where the return keyword is used. That’s why “promise in” and “promise out” were output first in the first example.

  • setImmediate – the callback function is called when switching context
  • Promise – the callback function is called immediately
Sponsored links

Promise with event loop doesn’t work without seam

When we use Promise in the following way Promise doesn’t work as expected. I defined Promise.then outside of the event loop. Expect behavior is that “resolved” is written while doing something in the event loop. However, if there is no seam in the event loop “then” is not triggered. You should read here for the detail. In the example below, the phase won’t change and it causes unexpected behavior.

// EventLoop.ts
console.log("Start")
const promise = new Promise((resolve) => {
    global.setTimeout(() => resolve("resolved"), 100);
});
console.log("Define promise.then")
promise.then((value) => {
    console.log(value);
})

console.log("Event loop starts")
while (true) {
    // Event loop
}

// Start
// Define promise.then
// Event loop starts

Promise example in practical case

Let’s consider a practical case. In a project which I work on, there is an API that requires a callback function to notify the result. The API is written in C++ and we created a wrapper so that we can call it in Typescript. However, the API doesn’t return the result. How can we implement this in this case? Let’s implement the example API.

// ProductApi.ts
export interface ProductInfo {
    productNumber: number;
    name: string;
    price: number;
}

export type ResultCallback = (value: ProductInfo) => void;

export function findProduct(productNumber: number, onValue: ResultCallback): void {
    global.setTimeout(() => {
        onValue({
            productNumber,
            name: `product name ${productNumber}`,
            price: productNumber * productNumber,
        });
    }, Math.random() * 1000);
}

When we want to get product information from product numbers 1 to 10 and display them we need to somehow call findProduct function 10 times. The first implementation is following but console.log shows an empty array because it takes a max 1 second to finish findProduct. We need to wait for it.

// ProductNumber.ts
export function getProductNumbers(): number[] {
    return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
}

// Practice1.ts
const productNumbers = getProductNumbers();
const productList: string[] = [];
productNumbers.forEach((productNumber) => {
    findProduct(productNumber, (value: ProductInfo) => {
        const format = `${value.productNumber}: ${value.name}: ${value.price}`;
        productList.push(format);
    });
});
console.log(productList);  // -> []

Let’s see the second implementation. In this example, displayResult function checks the result every 100 ms and it displays the result when all calls finish. But the order is not sorted because it timing for each function call is different.

// Practice2.ts
const productNumbers = getProductNumbers();
const productList: string[] = [];
productNumbers.forEach((productNumber) => {
    findProduct(productNumber, (value: ProductInfo) => {
        const format = `${value.productNumber}: ${value.name}: ${value.price}`;
        productList.push(format);
    });
});
const displayResult = () => {
    const timeout = 100;
    global.setTimeout(() => {
        if (productList.length === 10) {
            console.log(productList);
        } else {
            displayResult();
        }
    }, timeout);
};
displayResult();


// [
//     '8: product name 8: 64',
//     '5: product name 5: 25',
//     '1: product name 1: 1',
//     '6: product name 6: 36',
//     '3: product name 3: 9',
//     '7: product name 7: 49',
//     '10: product name 10: 100',
//     '9: product name 9: 81',
//     '2: product name 2: 4',
//     '4: product name 4: 16'
//   ]

This is not so nice because we need to implement two additional things.

  • Wait for the function completion
  • Sorting

Is there any better way for this? Yes, there is. Let’s create a function to make findProduct function Promise. It looks like this below.

// MyExtendedApi.ts
export function findProductAsync(productNumber: number): Promise<ProductInfo> {
    return new Promise((resolve) => {
        findProduct(productNumber, (value: ProductInfo) => {
            resolve(value);
        });
    });
}

If we use await keyword in the caller side we can write the logic in the same way as Synchronous. It looks something like this. It looks much better than before. But wait. Does the following code work? The answer is NO!

// Practice3.
const productNumbers = getProductNumbers();
const productList: string[] = [];
productNumbers.forEach(async (productNumber) => {
    const info = await findProductAsync(productNumber);
    const format = `${info.productNumber}: ${info.name}: ${info.price}`;
    productList.push(format);
});
console.log(productList); // -> []

async/await with forEach doesn’t work

To get straight to the point, await doesn’t work in Array.forEach function because forEach function expects that the callback is synchronous. When await keyword is used on a statement the function return type becomes Promise. When the function return type is Promise the function returns the Promise object at the line where await is used. Let’s have a further look. I’m not sure the actual implementation of forEach but I guess it looks like this. Can you imagine what the output is?

// AsyncCallback.ts
function requireCallback(callback: () => void): void {
    callback();
}

console.log("start")
requireCallback(async () => {
    const promise = new Promise((resolve) => {
        setTimeout(() => {console.log("in callback")}, 100);
    });
    await promise;
});
console.log("end")

// start
// end
// in callback

Was your thought correct? “in callback” was displayed at the end because await keyword is not used on the line calling requireCallback. That’s the important point which we should know. Calling the Promise function without await or then/catch means we call the function but throw it away. It doesn’t make sense to use the async/await keyword in the callback function if the top-level function calls it without await keyword. Let’s go back to forEach. forEach calls the callback as many times as the Array length.

// Foreach.ts
function doSlowProcess(a: number): Promise<number>{
    return new Promise((resolve) => {
        global.setTimeout(() => {resolve(a)}, 100);
    });
}
let result = 0;
[1,2,3,4,5].forEach(async (value) => {
    result = await doSlowProcess(value);
});
console.log(result); // -> 0

The expected result is 15 but the actual result was 0 because forEach doesn’t wait for the result of sum function.

This article may be helpful if you want to know more about the difference between loop statements.

Promise with for or while instead of forEach

An easy solution for the problem above is to switch from forEach to for/while loop. In this way, we don’t have to implement the two logic which I described above.

// Practice4.ts
async function doWithFor() {
    console.log("do with for start");
    const productNumbers = getProductNumbers();
    const productList: string[] = [];
    for (const productNumber of productNumbers) {
        const info = await findProductAsync(productNumber);
        const format = `${info.productNumber}: ${info.name}: ${info.price}`;
        productList.push(format);
    }
    console.log(productList);
    console.log("do with for end");
}
const start = performance.now();
doWithFor()
    .finally(() => {
        console.log(`time: ${performance.now() - start}`);
    });

// do with for start
// [
//   '1: product name 1: 1',
//   '2: product name 2: 4',
//   '3: product name 3: 9',
//   '4: product name 4: 16',
//   '5: product name 5: 25',
//   '6: product name 6: 36',
//   '7: product name 7: 49',
//   '8: product name 8: 64',
//   '9: product name 9: 81',
//   '10: product name 10: 100'
// ]
// do with for end
// time: 6677.618099998683

await Promise all

There is a better solution to improve the performance. The previous way is to call findProductAsync one by one, therefore it took about 7 seconds but actually, we want to call it 10 times without waiting for the previous function call’s completion. We can use Promise.all function for this purpose. Promise.all put the result in the same order as the input order. productList is already sorted after it receives the result! In addition to that, map function calls findProductAsync function without waiting for the completion. Instead, it immediately returns the Promise object. Following code is the best way to do in this example.

// Practice4.ts
async function doWithMap() {
    console.log("do with map start");
    const productNumbers = getProductNumbers();
    const promises = productNumbers
        .map((productNumber) => findProductAsync(productNumber));
    const productList = await Promise.all(promises);
    console.log(productList);
    console.log("do with map end");
}

const start = performance.now();
doWithMap()
    .finally(() => {
        console.log(`time: ${performance.now() - start}`);
    });

// do with map start
// [
//   { productNumber: 1, name: 'product name 1', price: 1 },
//   { productNumber: 2, name: 'product name 2', price: 4 },
//   { productNumber: 3, name: 'product name 3', price: 9 },
//   { productNumber: 4, name: 'product name 4', price: 16 },
//   { productNumber: 5, name: 'product name 5', price: 25 },
//   { productNumber: 6, name: 'product name 6', price: 36 },
//   { productNumber: 7, name: 'product name 7', price: 49 },
//   { productNumber: 8, name: 'product name 8', price: 64 },
//   { productNumber: 9, name: 'product name 9', price: 81 },
//   { productNumber: 10, name: 'product name 10', price: 100 }
// ]
// do with map end
// time: 1141.6266000010073

Conclusion

In this post, I explained how to use Promise object. Promise object with forEach function is one of the traps that beginners fall into. By using Promise, we can improve performance and even code readability!! However, it may make an unit test a little harder. However, it brings a lot of benefits to us.

Comments

Copied title and URL