Typescript: Errors are not bubbled up in async/await

Created on 12 Nov 2015  Â·  4Comments  Â·  Source: microsoft/TypeScript

I'm trying out async await right now. I thought this code should work:

async function f1() {
    return new Promise(async (resolve, reject) => {
        await f2();
        resolve();
    });
}

async function f2() {
    return new Promise((resolve, reject) => {
         throw new Error('error should bubble up');
    });
}

async function main() {
    try {
        await f1();
    }
    catch(err) {
        console.log(err.message)
    }
}

main();

But instead, I would need to add a try/catch in f1's promise in order to make the error bubble up. I thought promises already catches and throw it upwards even on await statements? Otherwise, people need to put a lot of try/catch statements everywhere.

Question

Most helpful comment

This is working as specified. The issue here is this:

async function f1() {
    return new Promise(async (resolve, reject) => {
        await f2();
        resolve();
    });
}

You are using an async arrow function as the executor callback to the Promise constructor. Even though the arrow function is async, the Promise constructor does not observe the return value of the callback (as per the ES6 specification). When f2() eventually throws, the exception rejects the Promise created by the async arrow function. Since the Promise constructor does not observe the Promise returned by the async arrow function, the rejection will be unobserved and f1 will be forever pending.

There are two ways to properly write f1. If you truly intended to return a Promise in f1, you could instead write it as follows:

async function f1() {
    return new Promise(async (resolve, reject) => {
        try {
            await f2();
            resolve();
        }
        catch (e) {
            reject(e);
        }
    });
}

However, if your intend was to simply await the result of f2, you could have written the function like this:

async function f1() {
    await f2();
    return;
}

Any async function automatically wraps its body in a new Promise, so there is no need to create one yourself. The only time you need to create a new Promise, is if you are consuming an asynchronous API that does not itself expose a Promise. For example, if you wanted to read a file asynchronously in NodeJS:

import { readFile } from "fs";

// marked 'async' as we are consuming 'f2' here
async function f1() {
    let contents = await f2();
    console.log(contents);
}

// not marked 'async' as we are manually creating a new Promise here
function f2() {
    return new Promise<string>((resolve, reject) => {
        readFile('somefile.txt', 'utf8', (error, data) => {
            if (error) {
                reject(error);
            }
            else {
                resolve(data);
            }
        });
    });
}

All 4 comments

Sorry, nothing wrong here.

I just tested this on babel, it seems like they throw an error in:

   return new Promise(async (resolve, reject) => {
        await f2();
        resolve();
    });
Unhandled promise rejection Error: error should bubble up(…)

https://babeljs.io/repl/#?experimental=false&evaluate=true&loose=false&spec=false&code=async%20function%20f1()%20%7B%0A%20%20%20%20return%20new%20Promise(async%20(resolve%2C%20reject)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20await%20f2()%3B%0A%20%20%20%20%20%20%20%20resolve()%3B%0A%20%20%20%20%7D)%3B%0A%7D%0A%0Aasync%20function%20f2()%20%7B%0A%20%20%20%20return%20new%20Promise((resolve%2C%20reject)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20throw%20new%20Error('error%20should%20bubble%20up')%3B%0A%20%20%20%20%7D)%3B%0A%7D%0A%0Aasync%20function%20main()%20%7B%0A%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20await%20f1()%3B%0A%20%20%20%20%7D%0A%20%20%20%20catch(err)%20%7B%0A%20%20%20%20%20%20%20%20console.log(err.message)%0A%20%20%20%20%7D%0A%7D%0A%0Amain()%3B

It is quite hard to find the error when it is hidden or catched in this case and not rethrowned in TS. Isn't babel doing the right thing here? TS just gives me no hints of what went wrong. I know TS guidelines is to not to inject any code that could affect the runtime. But this case feels like an exception that could be made?

I'm pretty sure this is per spec, but @rbuckton and @bterlson would know better than I do.

This is working as specified. The issue here is this:

async function f1() {
    return new Promise(async (resolve, reject) => {
        await f2();
        resolve();
    });
}

You are using an async arrow function as the executor callback to the Promise constructor. Even though the arrow function is async, the Promise constructor does not observe the return value of the callback (as per the ES6 specification). When f2() eventually throws, the exception rejects the Promise created by the async arrow function. Since the Promise constructor does not observe the Promise returned by the async arrow function, the rejection will be unobserved and f1 will be forever pending.

There are two ways to properly write f1. If you truly intended to return a Promise in f1, you could instead write it as follows:

async function f1() {
    return new Promise(async (resolve, reject) => {
        try {
            await f2();
            resolve();
        }
        catch (e) {
            reject(e);
        }
    });
}

However, if your intend was to simply await the result of f2, you could have written the function like this:

async function f1() {
    await f2();
    return;
}

Any async function automatically wraps its body in a new Promise, so there is no need to create one yourself. The only time you need to create a new Promise, is if you are consuming an asynchronous API that does not itself expose a Promise. For example, if you wanted to read a file asynchronously in NodeJS:

import { readFile } from "fs";

// marked 'async' as we are consuming 'f2' here
async function f1() {
    let contents = await f2();
    console.log(contents);
}

// not marked 'async' as we are manually creating a new Promise here
function f2() {
    return new Promise<string>((resolve, reject) => {
        readFile('somefile.txt', 'utf8', (error, data) => {
            if (error) {
                reject(error);
            }
            else {
                resolve(data);
            }
        });
    });
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

dlaberge picture dlaberge  Â·  3Comments

uber5001 picture uber5001  Â·  3Comments

bgrieder picture bgrieder  Â·  3Comments

kyasbal-1994 picture kyasbal-1994  Â·  3Comments

zhuravlikjb picture zhuravlikjb  Â·  3Comments