Source:
https://mrcoles.com/javascript-promises-and-errors/
You can definitely make some mistakes.
Here are a bunch of examples of what happens in different scenarios to help get a sense for what happens when.
1. Normal errors
// a synchronous error thrown outside the promise, raises an exception
// that must be caught with try/catch
function example() {
throw new Error("test error outside");
return new Promise((resolve, reject) => {
resolve(true);
});
}
try {
example()
.then(r => console.log(`.then(${r})`))
.catch(e => console.error(`.catch(${e})`));
} catch (e) {
console.error(`try/catch(${e})`);
}
// > Output:
//
// try/catch(Error: test error outside)
2. Errors inside Promises
// an error thrown inside the promise, triggers .catch()
function example() {
return new Promise((resolve, reject) => {
throw new Error("test error inside");
resolve(true);
});
}
try {
example()
.then(r => console.log(`.then(${r})`))
.catch(e => console.error(`.catch(${e})`));
} catch (e) {
console.error(`try/catch(${e})`);
}
// > Output:
//
// .catch(Error: test error inside)
3. Calling reject(…)
// explicitly calling reject, triggers .catch()
function example() {
return new Promise((resolve, reject) => {
reject("test reject");
});
}
try {
example()
.then(r => console.log(`.then(${r})`))
.catch(e => console.error(`.catch(${e})`));
} catch (e) {
console.error(`try/catch(${e})`);
}
// > Output:
//
// .catch(test reject)
4. Not specifying a .catch(…)
// failing to catch a reject means the code will continue to execute
// as if everything was fine, except it prints a warning
//
// in the future it will be a runtime error that terminates the process
function example() {
return new Promise((resolve, reject) => {
reject("test reject");
});
}
try {
example().then(r => console.log(`.then(${r})`));
} catch (e) {
console.error(`try/catch(${e})`);
}
// > Output:
//
// (node:25692) UnhandledPromiseRejectionWarning: test reject
// (node:25692) 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: 2)
// (node:25692) [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.
5. Not specifying a .catch(…) when Promise resolves ok
// the UnhandledPromiseRejectionWarning only triggers when an
// unhandled promise actually occurs. In the example below it
// appears fine, but future hidden errors may be lurking
//
function example() {
return new Promise((resolve, reject) => {
resolve("test resolve");
});
}
try {
example().then(r => console.log(`.then(${r})`));
} catch (e) {
console.error(`try/catch(${e})`);
}
// > Output:
//
// .then(test resolve)
Let’s now look at some examples with the es7 async await
syntax. Essentially, any function that is defined as async
automatically returns a promise where the .then(…)
contains its return value or if an error is thrown, then .catch(…)
is triggered with the error it experiences.
Furthermore, you can use await
inside an async
function to automatically resolve the line as a promise. This will evaluate to the value inside the .then(…)
of that promise or throw an error with the value inside the .catch(…)
of that promise. Likewise, any errors bubble up in the same way that a reject
call would, which allows for the same handling of synchronous errors and promise rejections.
6. Async functions and synchronous errors
// all async functions return promises and any errors that
// are thrown automatically trigger their catch method
//
async function example() {
throw new Error("test error at top of example");
}
try {
example()
.then(r => console.log(`.then(${r})`))
.catch(e => console.log(`.catch(${e})`));
} catch (e) {
console.error(`try/catch(${e})`);
}
// > Output:
//
// .catch(Error: test error at top of example)
7. Async functions and synchronous errors inside an await
// using await expects and parses the returned promise.
// If the function throws an error outside the promise,
// this gets thrown inside the async function and that
// bubbles up to the catch of its own promise.
//
async function example() {
return await inner();
}
const inner = () => {
throw new Error("test error outside promise");
return new Promise((resolve, reject) => {
resolve(true);
});
};
try {
example()
.then(r => console.log(`.then(${r})`))
.catch(e => console.log(`.catch(${e})`));
} catch (e) {
console.error(`try/catch(${e})`);
}
// > Output:
//
// .catch(Error: test error outside promise)
8. Async functions and errors inside an await’s Promise
// If the promise returned in an await triggers a
// catch, then that also gets thrown as an error inside
// the async function and once again it bubbles up to the
// catch of its own promise.
//
async function example() {
return await inner();
}
const inner = () => {
return new Promise((resolve, reject) => {
throw new Error("test error inside promise");
resolve(true);
});
};
try {
example()
.then(r => console.log(`.then(${r})`))
.catch(e => console.log(`.catch(${e})`));
} catch (e) {
console.error(`try/catch(${e})`);
}
// > Output:
//
// .catch(Error: test error inside promise)
9. Async functions and calling reject(…) inside an await’s Promise
Left as a practice for the reader! (Spoiler alert: it’s the same result as the prior example.)
Some takeaways
- Always add a
.catch(…)
whenever you have a .then(…)
—you don’t want an unhandled Promise rejection! A common exception to this is if you’re returning a promise from a function and you expect whatever is calling that function to handle the catch. In that way, you can have one catch at the end of a bunch of promises.
- If you expect a function to synchronously throw an error before a Promise, then you’ll want to wrap it in a
try-catch
, however, you likely don’t want functions to work like this and more likely such an error will be a coding bug that should be raised. (Excercise for the reader, explore the approach of starting your function with Promise.resolve(() => { … })
and putting the synchronous code in there.)
- Inside
async
functions, synchronous errors and Promise rejections all bubble up as a Promise rejection in the function’s returned Promise object
Further exploration
Try out chaining multiple .then(…)
calls in a row. This avoids indenting ever deeper as you chain more calls. Both .then(…)
and .catch(…)
return a Promise, whether you return Promise or a regular variable from the from either. See how a Promise rejection will short-circuit a chain of then
calls to the next catch
. This also can simplify the your handling of Promise rejections to just one .catch(…)
with many Promises.
Chain .then(…)
calls after a .catch(…)
and trigger different paths of execution.
See what happens when you throw an error inside a .catch(…)
. You’ll get another unhandled Promise rejection!
Play around with how returning a value within a .then(…)
keeps the Promise going and allows you to chain without embedding deeper and deeper.
-
-
Thank you for making it to the end! LMK if anything weird happens with the gifs, I just added the paused state and the play buttons using HTML5 canvas.