5

Searching for "Uncaught (in promise)" in Angular's github issues, google or here on stackoverflow yields a lot of very specific results, but my question is a bit more broad (and since the proliferation of results, I don't know if this is a duplicate)

Premise: somewhere in my code I have an uncaught promise rejection (Angular 4.4.4)

Cases:

Case 1: Simple Rejection

In MyComponent:

ngOnInit() {
    Promise.reject("foo");
}

Yields this in the console (great, I can see where it came from):

ERROR Error: Uncaught (in promise): foo
    at resolvePromise (zone.js:824)
    at Function.ZoneAwarePromise.reject (zone.js:903)
    at MyComponent.webpackJsonp.../../../../../src/app/component/my.component.ts.MyComponent.ngOnInit (my.component.ts:nn)

Case 2: new Promise, throwing an error

In MyComponent:

ngOnInit() {
  new Promise((resolve, reject) => {
    throw new Error("foo");
  }).then(() => {
    console.log("ain't gonna happen");
  });
}

Yields this in the console (even better, gives away the location right on the spot):

ERROR Error: Uncaught (in promise): Error: foo
Error: foo
    at my.component.ts:nn
    at new ZoneAwarePromise (zone.js:890)
    at MyComponent.webpackJsonp.../../../../../src/app/component/my.component.ts.LMyComponent.ngOnInit (my.component.ts:nn)

This also works with chaining, e.g. the following gives the same stack trace more or less in the console.

ngOnInit() {


  const promise3 = new Promise((resolve, reject) => {
    throw new Error("baz");
  });
  const promise2 = new Promise((resolve, reject) => {
    resolve(promise3);
  });

  const promise1 = new Promise((resolve, reject) => {
    resolve(promise2);
  });

  promise1.then(p1=> console.log(p1));
}

Case 3: Using reject(...)

In MyComponent:

ngOnInit() {
  new Promise((resolve, reject) => {
    reject("foo");
  }).then(() => {
    console.log("ain't gonna happen");
  });
}

Changing throwing an error to the (I assume pretty common practice) of using the promise's reject method, yields this horror:

core.es5.js:1020 ERROR Error: Uncaught (in promise): baz
    at resolvePromise (zone.js:824)
    at resolvePromise (zone.js:795)
    at zone.js:873
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:425)
    at Object.onInvokeTask (core.es5.js:3881)
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:424)
    at Zone.webpackJsonp.../../../../zone.js/dist/zone.js.Zone.runTask (zone.js:192)
    at drainMicroTaskQueue (zone.js:602)
    at <anonymous>

Question

  1. Why can't I see the stack trace or the origin of the error this time? (I assume it's due to simply JavaScript only generating stack trace from the location of the Error thrown?)

  2. If so, is it bad practice to use explicit reject over throwing a new Error in a new Promise? Is it documented somewhere and I missed it?

  3. Is there any way to fix this? (get the stack trace and keep using reject(...) instead of throwing an Error?, I assume no)

  4. Is this a well known issue / bad practice and I just missed the memo?

Eran Medan
  • 44,555
  • 61
  • 184
  • 276

1 Answers1

0

Partially answering one of my own questions:

If I change case #3 to this: (wrapping in a new Error object)

ngOnInit() {
  new Promise((resolve, reject) => {
    reject(new Error("foo"));
  }).then(() => {
    console.log("ain't gonna happen");
  });
}

Then, surprise surprise, I do get the stack trace! (merely constructing an error saves the stack trace)

On a side note... since (I'm guessing) most of us prefer seeing a stack trace vs not, then why oh why isn't that mentioned as a best practice here?: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

Edit

To make sure I don't forget, I added this narrowing (reject: (reason: Error) => void instead of any), not sure if this is a great solution as it doesn't safeguard from passing any, but it can help safeguard some of the cases of not passing an Error to reject.

export {}

declare global {

  interface PromiseConstructor {
    /**
     * A reference to the prototype.
     */
    readonly prototype: Promise<any>;

    /**
     * Creates a new Promise.
     * @param executor A callback used to initialize the promise. This callback is passed two arguments:
     * a resolve callback used resolve the promise with a value or the result of another promise,
     * and a reject callback used to reject the promise with a provided reason or error.
     */
    new <T>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason: Error) => void) => void): Promise<T>;

  }
}
Eran Medan
  • 44,555
  • 61
  • 184
  • 276