Promise rejection throws warning even if it's being caught later

Viewed 237

Example

class Foo {
    private pro = new Promise(() => {
                      throw new Error();
                  });

    public usePro() {
        return this.pro.then(() => {});
    }
}

let foo = new Foo();
setTimeout(() => {
    foo.usePro().then(() => {
        console.log("end.");
    }).catch(() => {
        console.log("error.");
    })
}, 1000);

I understand that javascript can't know at runtime that someone will catch the error later, so how am I suppose to do in such a situation ?

Console

(node:39166) UnhandledPromiseRejectionWarning: error
(node:39166) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:39166) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
error.
(node:39166) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
3 Answers

Errors should be caught wherever the Promise is used, even if that Promise is returned (and caught) by something else later. One option would be to assign to this.proValue a resolved object or a rejected object, depending on whether the original Promise resolves or rejects. Then, when usePro is called, check this.proValue and return either Promise.resolve(resolved) or Promise.reject(rejected). Using standard Javascript so this can be shown in a runnable snippet:

class Foo {
  constructor() {
    this.pro = new Promise(() => {
      throw new Error('Problem!');
    })
    .then((resolved) => {
      this.proValue = { resolved };
    })
    .catch((rejected) => {
      this.proValue = { rejected };
    });
  }

  usePro() {
    const { resolved, rejected } = this.proValue;
    if (resolved) return Promise.resolve(resolved);
    else if (rejected) return Promise.reject(rejected);
  }
}

const foo = new Foo();
setTimeout(() => {
  foo.usePro().then(() => {
    console.log("end.");
  }).catch((e) => {
    console.log("error caught. " + e);
  })
}, 1000);

If you want to be able to call usePro before Foo's internal Promise has resolved (or rejected), then when usePro is called, construct and return a Promise that resolves once this.pro's Promise resolves (or rejects). unfortunately the code required is moderately more complicated:

class Foo {
  constructor() {
    this.callProms = [];
    setTimeout(() => {
      this.pro = new Promise(() => {
        throw new Error('Problem!');
      })
      .then((resolved) => {
        this.proValue = { resolved };
      })
      .catch((rejected) => {
        this.proValue = { rejected };
      })
      .finally(() => {
        console.log('internal promise finishing');
        this.resolveCalls();
      });
    }, 1000);
  }
  resolveCalls() {
    this.callProms.forEach((resolve) => {
      resolve(this.getProValue());
    });
  }
  getProValue() {
    const { proValue } = this;
    if (!proValue) return;
    const { resolved, rejected } = proValue;
    if (resolved) return Promise.resolve(resolved);
    else if (rejected) return Promise.reject(rejected);
  }
  usePro() {
    return this.getProValue()
    || new Promise((resolve) => {
      this.callProms.push(resolve);
    });
  }
}

console.log('Starting');
const foo = new Foo();

// Immediate call of `usePro`:
foo.usePro().then(() => {
  console.log("end.");
}).catch((e) => {
  console.log("immediate error caught. " + e);
})

// Delayed call:
setTimeout(() => {
  foo.usePro().then(() => {
    console.log("end.");
  }).catch((e) => {
    console.log("delayed error caught. " + e);
  })
}, 2000);

Great answer by CertainPerformance.

Let me add that in Node.js, you can also add an unhandledRejection listener on process:

process.on('unhandledRejection', reason => {
    console.error({Error:reason})
    process.exit(1);
});

You could use Promise.all to delay the resolving:

  const delay = ms => new Promise(res => setTimeout(res, ms));

  Promise.all([
      foo.usePro(),
      delay(1000)
  ]).then(() => { 
     console.log("end.");
  }).catch(() => { 
     console.log("error."); 
  });

That way the .catch is directly attached, but the then callback is executed after the delay.

Related