Backend | Part 3 — Error Handling

Status CodeDescription
2xx (Success)200Operation succeeded
201Success, resource created
3xx (Redirect)301Moved permanently
4xx (Client-side error)401Not authenticated
403Not authorized
404Not found
422Invalid input
5xx (Server-side error)500Server-side error

Part 1: Error Object

Throwing native JavaScript Error objects as opposed to string literals or other types of values has several advantages:

  1. 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 you throw something other than an Error object.
  2. 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.
  3. Compatibility: Throwing native Error objects is generally more compatible with error-handling libraries and built-in language features like async/await and Promise error handling.
  4. 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;
  1. 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 or AuthenticationError that inherits from the native Error 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...');
  1. 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 CodeDescription
2xx (Success)200Operation succeeded
201Success, resource created
3xx (Redirect)301Moved permanently
4xx (Client-side error)401Not authenticated
403Not authorized
404Not found
422Invalid input
5xx (Server-side error)500Server-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);