-
-
Couldn't load subscription status.
- Fork 0
Style Guides ‐ Errors
The
try...catchstatement is comprised of atryblock and either acatchblock, afinallyblock, or both. The code in thetryblock is executed first, and if it throws an exception, the code in thecatchblock will be executed. The code in thefinallyblock will always be executed before control flow exits the entire construct.
Source: MDN
try {
/* work happens here */
} catch (err) {
// ^---^ "catch binding" optional since ES2019/Node 10
/* deal with the error */
} finally {
/* clean up whether or not there was an error */
}Although anything can be thrown, e.g. throw "oh no!" is legal JS, throwing a proper Error object means the full context can be retained and a proper traceback generated.
Don't:
throw "something went wrong";Do:
throw new Error("something went wrong");Sometimes we want different things to happen depending on what error occurred. One option for this is creating specific subclasses of Error to represent these states.
Do:
// define Error subclass
class MissingResource extends Error {
constructor(id) {
super(`Resource not found: ${id}`);
}
}
// throw the subclass as needed
throw new MissingResource(resourceId);
// handle the subclass explicitly
try {
// do some stuff
} catch (err) {
if (err instanceof MissingResource) {
// handle this specific kind of error
}
// handle any other errors
}Example of this pattern in use in the Tech Products Demo app here.
Another option for discriminating different kinds of error, as used throughout Node.js itself, is to have a specific code property intended for use in code (whereas the message property is intended to be human-readable).
Do:
// defined subclass for errors with codes
class CodedError extends Error {
constructor(code, message) {
super(message);
this.code = code;
}
}
// throw the subclass as needed
throw new CodedError("RESOURCE_MISSING", `Resource not found: ${resourceId}`);
// handle the coded case explicitly
try {
// do some stuff
} catch (err) {
if (err.code === "RESOURCE_MISSING") {
// handle this specific kind of error
}
// handle any other errors
}We don't want to leak details of internal issues to end-users of our APIs, but it's still useful for devs to be able to get more data when something goes wrong in production.
It's common in Express APIs to have a catch-all error handling in the controller, to ensure we send a sensible response to the request in all cases. But we also need to make sure that we'll have enough information when things go wrong, by reviewing the actual logs from the application. That means logging the error before responding, so the details show up in e.g. Papertrail and/or the Kubernetes pod logs.
Don't:
export const handleRequest = (req, res) => {
try {
// ...
} catch {
res.status(400).send({ success: false, error: "Something went wrong!" });
}
};Do:
export const handleRequest = (req, res) => {
try {
// ...
} catch (err) {
console.error(err); // or use a proper logger, if available
res.status(400).send({ success: false, error: "Something went wrong!" });
}
};or:
export const handleRequest = (req, res, next) => {
try {
// ...
} catch (err) {
if (/* we can respond here */) {
// ...
} else {
next(err); // let other middleware handle it
}
}
};Sometimes having a try construct at all is unnecessary. Remember that the caller can handle the errors.
A clear sign of an unnecessary try is that the only thing in catch is re-throwing the error. In such cases, the try can be removed entirely (or the catch block can be removed, if finally is being used) - it makes it clearer that the code in question will not be attempting to handle any errors and they'll propagate up to the caller.
Don't:
try {
// do some things
} catch (err) {
throw err; // if this is the _only_ thing here, it's pointless
}Do:
// do some thingsor:
try {
// do some things
} catch (err) {
// do some things with `err`
throw err; // optionally re-throw for any case we can't handle
}Sometimes we can handle some errors, but want to re-throw in other cases. If this is done incorrectly, though, the context of the original problem is lost.
The first argument to the Error constructor is the message, which should be a string. If an error object is passed instead, it is stringified (e.g. new Error("oh no!") -> "Error: oh no!") and all of the context is lost. Instead, if more context is needed, give a proper message and use the second argument to pass a cause.
Don't:
throw new Error(err);Do:
throw err; // re-throw with original contextor:
throw new Error("an additional message", { cause: err }); // add more contextor:
throw new Error("a separate message"); // explicitly drop original context
throw new Error(err.message); // drop original context but keep message- 📖 Error-related ESLint rules:
- 📖 Node.js errors