1

The following code is an MCVE of my problem:

class MyApp {
    public async crash(): Promise<number> { 
        throw new Error("Hello Error");
    }

    public async chain(promise: Promise<number>) {
        await this.delay(10000);
        return promise;
    }

    private async delay(ms: number): Promise<void> {
       return new Promise<void>( resolve => setTimeout(resolve, ms) );
    }
}

const myApp = new MyApp();

const a = async () => { 
    try {
        const foo = await myApp.chain(myApp.crash());
    } catch (e) {
        console.log("Hello Catch");
    }
};

a();

Link to Typescript Playground

I have a Promise<>, that sooner or later will reject. I have a handler that will await said promise. However, if the rejection is faster then my code leading to the await call, some global error handler will kick in and end my app, telling me I had an uncaught error. Funny side effect: if I wait long enough (10s in my example), it will magically recover and remember that there was an await and a catch block and actually do what's in the catch.

But for those 10 seconds, my script is in the "we are all gonna die!" mode. Some global error handler kicks in, my app is shut down.

What am I doing wrong? Isn't the whole point of being async that I don't have to handle it right now? Why is there even a global error handler?

My real world case is this:

public async showWait<T>(promise: Promise<T>): Promise<T> {
    await loadingSpinner.present();

    try {
        return await promise;
    }
    finally {
        await loadingSpinner.dismiss();
    }
}

If the task to execute crashes faster than I can show a spinner, my whole app will crash. And before you jump in and tell me to add a catch block, please see the MCVE, that's not the problem. I do catch the exception from the awaithigher up the call chain, but the crash happens before the code even reaches the await.

nvoigt
  • 75,013
  • 26
  • 93
  • 142

1 Answers1

4

The problem is that you are constructing a promise and not doing anything with it (not installing an error handler!) while waiting for some other promise in the mean time:

let promise = crash();
await loadingSpinner.present();
await promise;

This is a well-known antipattern: Waiting for more than one concurrent await operation

In general your methods should accept values not promises, but in this case it might be warranted if you want to wrap the promise. You still need to immediately handle it though. The proper solution would be

public async showWait<T>(promise: Promise<T>): Promise<T> {
    try {
        const [, val] = await Promise.all([
           loadingSpinner.present(),
           promise,
        ]);
        return val;
    } finally {
        await loadingSpinner.dismiss();
    }
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Thank you. I'm coming from C# where that is not an anti-pattern because the task does not care for handlers and the await will throw if it deems it necessary. The tutorials I read used the same pattern so I did not really think about it :( I guess I need to update my google-a-good-tutorial skills :) – nvoigt Feb 28 '19 at 15:35
  • 1
    @nvoigt You're right, it's not *well*-know unfortunately. JS does have an unhandled rejection warning so that you cannot easily miss errors, but it gets in the way when you want to keep a rejected promise around and only handle it later. You can [silence it](https://stackoverflow.com/a/45895514/1048572) though. – Bergi Feb 28 '19 at 15:41