Welcome to part 4. Follow along this series to build a 3-tier Login System implementing core features required of an effective and usable authentication system.
Summary: In this tutorial, you will learn to create user-defined exceptions and handle these exceptions (or other unexpected events) as they occur through proper error handling in express.
Note 🔔:
If you would like to jump ahead to the finally built authentication system, the complete Git Repo can be found on Github✩.
With how Javascript engine works, when a runtime error occurs, execution will stop and an error is generated. In technical parlance, a runtime error causes new Error
object to be created and thrown. In such an event, the rest of code is not executed.
Well, that is how Javascripts handles itself when you write bad code. Likewise, the application you are building needs to handle itself when it receives bad input. Or when other unexpected events occur, including a runtime error.
For example, division by zero is not an error in Javascript. But logic in your application requires that it should be. The throw
keyword allows you to stop javascript execution whilst generating a custom defined exception. As you generate exception, you should handle it and communicate back to the user with succinct information about what went wrong.
Table of Contents
Outline of what we will do:
- Create custom error constructors.
- Create error/exception handlers.
- Configure express application to use custom error handlers.
- Error Handling in action.
In an error flow control, we will throw
using a custom error constructor, to generate a user-defined exception. The generated exception will be processed in error handler.
Directory structure
📂server/
└── 📂src/
├── 📄index.js
└── 📂config/
+ ├── 📂errors/
+ │ ├── 📄AuthorizationError.js
+ │ └── 📄CustomError.js
+ └── 📂exceptionHandlers/
+ └── 📄handler.js
This is the structure in regard to the src
directory that contains our code. We will update these files with code to provide the logic we want to achieve. You can always double check to create these files in their correct locations.
1. Create custom error constructors
Certainly you may be familiar with the in-built Error constructor, i.e new Error()
, which actually creates an Error object.
Let's create custom error constructor which is a superset of the in-built Error
, by which we will be able to add more properties to the created Error Object we will need later.
First, open the file: CustomError.js
. According to the directory structure for this tutorial(refer above), it should be at the path: server/src/config/errors/
. Create the constituent directories if you do not have.
Inside the opened file, paste in the following code:
class CustomError extends Error {
/**
* Custom Error Constructor
* @param {any} [message] - Optional error payload
* @param {number} [statusCode] - Optional error http status code
* @param {string} [feedback=""] - Optional feedback message you want to provide
*/
constructor(message, statusCode, feedback = "") {
super(message);
this.name = "CustomError";
this.status = statusCode;
this.cause = message;
this.feedback = String(feedback);
}
}
module.exports = CustomError;
The snippet above simply creates a class that extends base Error
class that is in-built to javascript. We define a constructor that passes message
as first argument to the base class(By calling super(message)
), and adds additional properties to the result Error Object when the class is instantiated.
To cause a user-defined exception in our application, we will throw CustomError()
. Arguments can be passed to define properties on created Error object.
Second, open/create the file: AuthorizationError.js
. Expected to be at the path: server/src/config/errors/
(refer above).
Paste the following code in the file:
const CustomError = require("./CustomError");
class AuthorizationError extends CustomError {
/**
* Authorization Error Constructor
* @param {any} [message] - Error payload
* @param {number} [statusCode] - Status code. Defaults to `401`
* @param {string} [feedback=""] - Feedback message
* @param {object} [authParams] - Authorization Parameters to set in `WWW-Authenticate` header
*/
constructor(message, statusCode, feedback, authParams) {
super(message, statusCode || 401, feedback); // Call parent constructor with args
this.authorizationError = true;
this.authParams = authParams || {};
this.authHeaders = {
"WWW-Authenticate": `Bearer ${this.#stringifyAuthParams()}`,
};
}
// Private Method to convert object `key: value` to string `key=value`
#stringifyAuthParams() {
let str = "";
let { realm, ...others } = this.authParams;
realm = realm ? realm : "apps";
str = `realm=${realm}`;
const otherParams = Object.keys(others);
if (otherParams.length < 1) return str;
otherParams.forEach((authParam, index, array) => {
// Delete other `realm(s)` if exists
if (authParam.toLowerCase() === "realm") {
delete others[authParam];
}
let comma = ",";
// If is last Item then no comma
if (array.length - 1 === index) comma = "";
str = str + ` ${authParam}=${this.authParams[authParam]}${comma}`;
});
return str;
}
}
module.exports = AuthorizationError;
AuthorizationError
is an error intended to be generated due to insufficient authentication.
It is extends the CustomError
class adding extra properties as well. Majorly, the constructor for this error class has a statusCode
that defaults to 401
which is passed to base class(CustomError
) when calling super()
.
Also, we have defined #stringifyAuthParams()
private method to convert authParams
object parameter to a list of comma separated strings of format key=value
. The formatted string is used to generate value for the authheaders
property.
New to private methods in javascript classes? Read more here.
Error object created using this class will be processed by error handler to produce a 401 - Unauthorized Error response that observes the web specifications, which require a response due to failed authentication include a WWW-Authenticate
header. Learn more here.
2. Create error/exception handlers
Here we will create handlers for unexpected events that occur in our application. These handlers are important to prevent a running program from shutting down unceremoniously to its users. Furthermore they provide concise information to the user about what went wrong.
We will create two error handlers:
- 404 case handler i.e handler called when http request on a path that does not exist hits our server.
- General exception handler i.e handler called when exceptions are generated from anywhere in our application.
Open the file: handler.js
. According to the directory structure for this tutorial(refer above), it should be at the path: server/src/config/exceptionHandlers/
. Create the constituent directories if you do not have.
Paste the following code inside the file:
// 404 Error Handler
function LostErrorHandler(req, res, next) {
res.status(404);
res.json({
error: "Resource not found",
});
}
// Exception Handler
function AppErrorHandler(err, req, res, next) {
res.status(err.status || 500);
if (err.authorizationError === true) {
// Sets headers available in Authorization Error object
res.set(err.authHeaders);
}
// `cause` is a custom property on error object
// that may contain any data type
const error = err?.cause || err?.message;
const providedFeedback = err?.feedback;
// respond with error and conditionally include feedback if provided
res.json({
error,
...(providedFeedback && { feedback: providedFeedback }),
});
}
module.exports = { LostErrorHandler, AppErrorHandler };
We have defined two functions here taking the signature of express middleware.
The first one, LostErrorHandler()
function is the 404 case handler. Importantly, it sets a 404
status code on the response(res
).
And the second one, AppErrorHandler()
function has 4 parameters which distinguishes it as an express error middleware. This is the General exception handler.
Properties are available on the err
parameter of the AppErrorHandler()
function depending on error constructor that created the Error object. For example we see this line:
// ...
if (err.authorizationError === true) {
res.set(err.authHeaders);
}
// ...
We know err
is an authorization error object when we throw
an exception using the custom AuthorizationError
constructor, like:
throw new AuthorizationError();
This will create an Error object that contains authorizationError
property set to true
. Also includes authHeaders
property e.t.c.
3. Configure express application to use custom error handlers.
The last step is to configure the express application to use error handlers we have created. For this, open index.js
which is the entrypoint to our application. This file should be located at server/src/
.
Add the following code to this file:
const express = require("express");
const {
AppErrorHandler,
LostErrorHandler,
} = require("./config/exceptionHandlers/handler.js");
/*
1. INITIALIZE EXPRESS APPLICATION 🏁
*/
const app = express();
const PORT = process.env.PORT || 8000;
/*
2. APPLICATION MIDDLEWARES AND CUSTOMIZATIONS 🪛
*/
// ...
/*
3. APPLICATION ROUTES 🛣️
*/
// Test route
app.get("/", function (req, res) {
res.send("Hello Welcome to API🙃 !!");
});
/*
4. APPLICATION ERROR HANDLING 🚔
*/
// Handle unregistered route for all HTTP Methods
app.all("*", function (req, res, next) {
// Forward to next closest middleware
next();
});
app.use(LostErrorHandler); // 404 error handler middleware
app.use(AppErrorHandler); // General app error handler
/*
5. APPLICATION BOOT UP 🖥️
*/
app.listen(PORT, () => {
console.log(`App running on port ${PORT}`);
});
In the fourth part of the snippet above, we have configured our app to handle 404 error case and general errors that may occur from anywhere in the application.
A few vital things to note:
- We have added error handling at part4 after routes for the application have been registered at part3. This way, incase of incoming request requiring resource on non-existent path, a 404 error handler will catch and process this error. And it is called only after no route has been matched in the routes registered at part3.
- We have added General error handler middleware as the last-most middleware in the express middleware chain. It is important to declare it last so that any unhandled errors that occur before it, can be captured and handled.
Hence when it comes to effective error handling in express, order of placement of the error handlers is important.
Cheers!🥂. Up to this point, your program should be equipped to gracefully handle unexpected errors that may occur in route handlers and middlewares.
Infact, let us test run by introducing error in our application.
4. Error Handling in action
To test Error Handling in our application, let's add another test route. So once again, open index.js
file which should be found at the path: server/src/
.
Edit part 3 this file to add a new route, like shown:
const CustomError = require("./config/errors/CustomError.js");
// ...
/*
3. APPLICATION ROUTES 🛣️
*/
// ...
// Test Crash route
app.get("/boom", function (req, res, next) {
try {
throw new CustomError("Oops! matters are chaotic💥", 400);
} catch (error) {
next(error);
}
});
// ...
We have added a new route,(/boom
) and its route handler.
Note: The route handler is written with the signature of express middleware, i.e it takes three parameters. Writing as an express middleware enables us to pass any errors generated here to the application's error handler we have configured using the next()
function, which is available as the third parameter in the route handler.
This route handler throw
s an error inside a try...catch
construct, using our predefined CustomError
class. We have passed a string message and a number for the HTTP status code we would like to return in the response. This will create an error object that will be caught by catch()
. Inside the catch(){...}
block, we simply forward the caught error to our configured error handling middleware with a single line of code: next(error)
. And that is it🦸.
Run the program. And when you run it, open your browser or any other client you prefer e.g postman. Then navigate to the route that throws an error, i.e http://localhost:8000/boom.
You should see that the error thrown in our application was handled deftly by error handler configured. And of course, our application is still running. It did not shut down.
Notice that the string message and HTTP status code passed to CustomError
constructor are included in the response body and header respectively:
Final thoughts
Well that is how you handle exceptions that can occur while running route handlers and middlewares in express. Possibly this is a compelling way since it reduces boiler plate code you need to write in the catch(){...}
block, especially in the route handlers. Additionally, "regular code" in route handlers is separated from "error handling code".
Using Custom Error constructors to create Error objects when we throw
is important for providing additional properties to the Error generated. The error handler can then obtain the properties on an error and respond to a user with concise information about what went wrong.
In the example where we test to see error-handling in action, we wrapped logic for the route handler inside try...catch
construct. If all we are going to write is synchronous code only, then we do not need to explicitly call next(error)
function with the error caught which is the case inside catch(){...}
block. Therefore we can throw
without the try...catch
construct and Express will automatically pass the error to error-handling middleware. But it is worthy to avoid writing code that is magical🪄.
However if you write asynchronous operations, you must call next(error)
function with the error returned, so it may be caught by the error-handling middleware. For example we may write a route handler that has asynchronous operation like this:
app.get("/boom", async function (req, res, next) {
const iAlwaysReject = new Promise((resolve, reject) => {
setTimeout(() => reject("Operation failed!"), 2000);
});
try {
await iAlwaysReject(); // Asynchronous OP
} catch (error) {
next(error); // Explicitly call next
}
});
Note: In Express 5 and above, asynchronous operations that reject or throw an error will call next(error)
function automatically with the thrown error or the rejected value. If no rejected value is provided, next
will be called with a default Error
object provided by the Express router. With above example, it means you can await without wrapping in try...catch
construct nor calling next(error)
explicitly. But why write code that is magical? 🤷♂️
Express ships with a default error handler. Of course it is not feature rich as you may want to create your own. I guess it is good enough to get you started.
Do you have other opinions? Drop them in the comment section🕳🤾. Meanwhile, I hope you found the article useful and...peace✌️.
Follow me @twitter. I share content that may be helpful to you😃.
Top comments (0)