Backend | Part 3 — Error Handling
Status Code | Description | |
---|---|---|
2xx (Success) | 200 | Operation succeeded |
201 | Success, resource created | |
3xx (Redirect) | 301 | Moved permanently |
4xx (Client-side error) | 401 | Not authenticated |
403 | Not authorized | |
404 | Not found | |
422 | Invalid input | |
5xx (Server-side error) | 500 | Server-side error |
Part 1: Error Object
Throwing native JavaScript Error
objects as opposed to string literals or other types of values has several advantages:
- Stack Trace: Native
Error
objects capture a stack trace, which can be extremely useful for debugging. The stack trace shows where the error was thrown and what function calls led up to it. This information is missing if youthrow
something other than anError
object. - Consistency: Using native
Error
objects is a common pattern in JavaScript, especially in larger codebases and libraries. It makes it easier for developers to understand what kinds of values they should expect when catching errors. - Compatibility: Throwing native
Error
objects is generally more compatible with error-handling libraries and built-in language features likeasync
/await
andPromise
error handling. - Additional Properties: Error objects can also contain additional properties that provide more context about the error. For instance, you could add a
status
property to an error object that corresponds to an HTTP status code.
const error = new Error('error message...');
error.status = 404;
- Extensibility: Native Error objects can be extended to create custom error types. This is useful for creating more descriptive and specific errors. For instance, you could create a
ValidationError
orAuthenticationError
that inherits from the nativeError
class.
class HttpError extends Error {
constructor(message, status) {
super(message);
this.status = status;
}
}
const error = new HttpError('error message...', 404);
class ValidationError extends Error {
constructor(message) {
super(message);
}
}
const validation_error = new ValidationError('validation error message...');
- Type Checking: When you catch errors, you can use
instanceof
to determine the type of error caught, which allows for more elegant error-handling logic.
try {
// some code
} catch (e) {
if (e instanceof ValidationError) {
// Handle validation errors
} else if (e instanceof HttpError) {
// Handle http errors
} else if (e instanceof TypeError) {
// Handle type errors
} else { // e instanceof Error or any other derived class not listed
// Handle all other errors
}
}
For these reasons, it's generally considered best practice to throw
instances of Error
or objects that inherit from Error
when you want to indicate an error condition in JavaScript.
Part 2: Error Handling Middleware with next(new Error(...))
Express error-handling middleware handles errors that occur in you Express application. It is defined using four arguments instead of the usual three: (err, req, res, next)
. This type of middleware should be defined last, after other server.use()
calls.
Just make sure your error-handling middleware is defined after your routes and other middleware to ensure that it catches errors from them.
Here's a simple example:
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('error message...');
});
To hit Express error-handling middleware, you can use any of the following methods:
2.1: Next Function
In your route handlers or other middleware, you can call the next
function with an Error
object. This will skip all other route and middleware functions and go directly to the error-handling middleware.
app.get('/some-route', (req, res, next) => {
const error = new Error('Something went wrong');
next(error);
});
2.2: Throwing an Error
In synchronous code, you can also throw
an error, and it will be caught by the error-handling middleware.
app.get('/some-route', (req, res, next) => {
throw new Error('Something went wrong');
});
2.3: Promise Rejection
If you're using asynchronous code, you can pass errors to the next
function inside a .catch()
block, or use async
/await
along with a try
/catch
block.
app.get('/some-route', async (req, res, next) => {
try {
// Some asynchronous code
} catch (error) {
next(error);
}
});
2.4: Explicitly Directing to Error-Handling Middleware
You can also define specific error-handling middleware for certain routes and direct errors there using next
.
When you explicitly direct to error-handling middleware, you are specifying which error-handling middleware should handle errors for a particular set of routes. In other words, you are scoping an error-handling middleware to a specific route or set of routes.
In the following example, the error will be handled by the error-handling middleware that is scoped to /some-route
, not the general error-handling middleware. This allows you finer control over how different kinds of errors are handled depending on which route they occur in.
app.get('/some-route', (req, res, next) => {
const error = new Error('Something went wrong');
next(error);
});
app.use('/some-route', (err, req, res, next) => {
// This error handler will catch errors from '/some-route'
res.status(500).send(err.message);
});
Using the next
function is a more general approach that will forward the error to the next available error-handling middleware, while explicitly directing to error-handling middleware is a more specific approach that allows you to control which error-handling middleware will handle errors for certain routes.
Part 3: HTTP Response Status Codes
Status Code | Description | |
---|---|---|
2xx (Success) | 200 | Operation succeeded |
201 | Success, resource created | |
3xx (Redirect) | 301 | Moved permanently |
4xx (Client-side error) | 401 | Not authenticated |
403 | Not authorized | |
404 | Not found | |
422 | Invalid input | |
5xx (Server-side error) | 500 | Server-side error |
Part 4: Making HTTP Requests via fetch
with Error Handling
async function asynch(promise) {
try {
const data = await promise;
return [data, null];
} catch(error) {
return [null, error];
}
}
const [data1, error1] = await asynch( fetch(url1) );
if (error1) handle(error1);
const [data2, error2] = await asynch( fetch(url2) );
if (error2) handle(error2);