Node error-handling

Learn how to handle different kinds of errors on your Node server

  • js
  • node
  • errors

Errors (or “exceptions”) stop JavaScript from running. They usually mean something has gone so wrong that the program doesn’t know how to continue. This means any code after the line where the error occurred won’t run. This is pretty bad in the browser as it can totally break the application for a single user, but on the server it can be much worse. A single running Node server might be responding to hundreds or thousands of requests from different users. If an error stops the code executing it stops for all of the users.

Heroku error page

JavaScript errors

“Error” can refer to both the “exception” (line of code going wrong) as well as the “error object” that is created. For example this code will cause an exception:

const myFunction = 2;
myFunction();
console.log("will not run");

If you run that in a browser you’ll see an error logged: TypeError: myFunction is not a function. You also will not see the log, as the JS stops executing your code when the exception occurs.

Different types of errors

The error we saw above was a “TypeError”. This is a more specific kind of error that is used when JavaScript expects to see one thing but got something else. In this case we tried to call a number as a function, which meant it was the wrong type of value.

There are several different kinds of built-in error. You can see a full list on MDN. Mostly you’ll see TypeError and ReferenceError (e.g. ReferenceError: myVariable is not defined).

Creating your own errors

We’ve seen how JS handles exceptions, but what about your own code? It’s possible to predict points in your code where something will go wrong and create your own error on purpose. For example if we write a function to square a number we can check whether the caller actually passed a number in:

function square(x) {
if (typeof x !== "number") throw new Error("Please pass a number!");
return x * x;
}

The throw keyword causes an exception in your code (just like when a built-in JS method breaks). You can throw anything (throw 5, throw "hello" etc), but it’s most common to create a new Error object and throw that. Error objects have a “stack trace”, which tells the user what line of code the error occurred on.

So now our square function behaves similarly to built-in JS methods: execution will stop if it encounters an invalid value. We can make this more specific by using a TypeError and passing a more useful message:

function square(x) {
if (typeof x !== "number") throw new TypeError(`${x} is not a number`);
return x * x;
}

square("hi"); // TypeError: hi is not a number

Error-handling

All these errors will just stop our program executing. Ideally we want to catch the error and handle it somehow (maybe by providing a message to the user).

We can use a try..catch block for this. We put all the code we think might error inside the try {} block, and if an error is thrown the catch (error) {} block runs with the error object.

try {
JSON.parse("< invalid json >");
} catch (error) {
console.error(error);
// SyntaxError: JSON.parse: unexpected character at line 1 column 2 of the JSON data
}

console.log("Continues to run fine);

Here we try to run our code, JSON.parse throws an error that we catch and log to the console. Since the error has been caught within this block the rest of our code can safely continue to run (so the final console.log still appears).

This works the same way for your errors you throw yourself, like our square function above:

function handleSubmit(event) {
const value = event.target.elements.num.value; // e.g. "hi"
try {
const squaredValue = square(value);
output.textContent = squaredValue;
} catch (error) {
console.error(error);
output.textContent = error.message;
// puts "hi is not a number" on the page
}
}

You can think of throwing an error as bypassing the rest of the code and jumping straight to the closest catch block.

Challenge

Let’s use try..catch to handle errors on a Node server.

  1. Download the starter files, cd in, run npm install
  2. Run npm run dev to start the server on http://localhost:3000
  3. Visit http://localhost:3000/try-catch. You should see Express’ default error response
  4. Use try..catch in the tryCatch route handler to catch the error and send your own response to the browser
    • The response should have a 500 status code and a message of Server error
    • Don’t fix the mistake in the tryCatch handler (it’s deliberate to simulate a real error)

Promise rejection

Errors often occur in asynchronous code, as this can involve network requests or file-system access (both of which can take a long time and lose connection partway through).

We can’t handle exceptions in asynchronous code using try..catch because the try block will have finished executing before the error occurs. For example:

try {
fetch("broken").then(console.log);
// then some more stuff
} catch (error) {
console.error(error);
}

If the fetch request takes 5 seconds the try will have finished executing long before the error occurs, which means the catch never runs. Promises don’t throw errors because that doesn’t work asynchronously. Instead they reject, which is the async equivalent.

Promises have a .catch method, which allows you to pass a function that runs if the promise is rejected.

fetch("broken")
.then(console.log)
.catch((error) => {
console.error(error);
// do stuff with the error here
});

It’s important to always have a .catch somewhere in a promise chain. Otherwise you’ll get an “unhandled rejection”, which could crash your program.

Challenge

Let’s handle a rejection. The server has a model.js file that pretends to access a database. However the getPosts function always rejects with an error (don’t fix this!).

  1. Visit http://localhost:3000/rejection
  2. Your browser should timeout waiting for a response
  3. Your server should log an error in your terminal: UnhandledPromiseRejectionWarning: Error: Retrieving posts failed
  4. Edit the route handler to catch the model.getPosts() promise rejecting
    • You should send a response with a 404 status code and a message of “Posts not found”

Bonus: unhandled Node exceptions

Unhandled exceptions are very dangerous on the server. They will cause the whole Node program to crash, preventing it from responding to more requests. This is actually a good thing—attempting to continue serving requests after an unhandled exception could lead to much worse issues, like saving incorrect data to a database or serving the wrong information to users.

This is why Node automatically stops your program on an unhandled exception. Unfortunately it does not do this for unhandled rejections (i.e. when a promise errors). This is why you see a warning when a promise rejects without a .catch:

In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

It’s a good idea to make your program stop on unhandled rejections too. You can do this by listening for an event on the global process object:

// in server.js
process.on("unhandledRejection", (error) => {
console.error(error);
process.exit(1);
});

The unhandledRejection event will fire when a promise rejects without being caught, and your callback function will run. We log the error, then tell the Node process to stop with an “exit code” of 1 (which means an error occurred). This will stop your server processing any more requests.

Add this to your server.js, then remove your .catch from the rejection handler function. Now when you visit http://localhost:3000/rejection and you shouldn’t see the “unhandled rejection” warning in your terminal. Instead your server should crash and stop processing further requests.

Note: it’s always better to handle the promise rejection properly in your route handler so you can send a response. This unhandledRejection listener is a last-ditch strategy because errors always slip through the cracks.

Recovering from unhandled exceptions

Ideally your server shouldn’t stay crashed: you want it to restart and continue handling requests. This has to be managed by something outside of the Node process.

If you have deployed your server to Heroku it will automatically try to restart your server if it crashed. If it crashes again it will wait up to 20 minutes before trying again, then keep waiting longer before each attempt. You can read about their crash restart policy.

If you’re managing your own Node deployment it’s common to use something like pm2 or systemd to automatically restart the process after it crashes.