Extend the Error Class in NodeJS
This is the approach that I use with an AWS Lambda Backend using NodeJS 14. My frontend is built using VueJS 3, Axios and Vuex.
We can extend the Error class in NodeJS to add whatever fields we need, in this case I cover the cause
key. I wanted to standardize my error code using human readable code, such as ‘INTEGRATION_NOT_UPDATED’, ‘INVALID_APPLICATION_NAME’, and so on…,
I think this solution will let me create efficient conditions in the frontend to show appropriate popup, redirect and etc.
I’ve also included the response logic to work with the provided solution in a AWS Api Gateway environment. It is a good idea to reduce the log quantity when going to production to avoid leaking information from the backend structure and in development having more will help developers to diagnose without having to go in CloudWatch every 5 minutes.
The code is only an example, you will not be able to copy paste, you need to revamp and adapt with your environment.
I’ve based this work on my custom NodeJS express framework:
Hope it helps you !
Code
errorHandler.js
// Studio Webux @ 2022
class ApiError extends Error {
constructor(message, name, code, extra, devMsg) {
super(message);
this.name = name || "UNKNOWN_ERROR";
this.cause = name || "UNKNOWN_ERROR";
this.code = code || 500;
this.extra = extra || {};
this.devMessage = devMsg || "";
}
}
module.exports = ApiError;
This following file is used to handle the AWS Api Gateway Lambda responses.
response.js
// Studio Webux @ 2021
const DEFAULT_HEADERS = {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": process.env.DOMAIN_NAME || "*",
"Access-Control-Allow-Methods": "GET,OPTIONS,POST",
"Access-Control-Allow-Headers": "Content-Type,X-Api-Key",
};
/**
* Format the response to work with API Gateway
* @param {Number} statusCode HTTP Code
* @param {Object} body Body object
* @param {Boolean} stringifyBody To use JSON.stringify
* @param {Boolean} isBase64Encoded
* @param {Object} customHeader to override the default header structure
*/
function response(
statusCode,
body,
stringifyBody = true,
isBase64Encoded = false,
customHeader = null
) {
const resp = {
statusCode: statusCode || 500,
body: stringifyBody
? body.body
? JSON.stringify(body)
: JSON.stringify({ body })
: body,
headers: customHeader || DEFAULT_HEADERS,
isBase64Encoded: isBase64Encoded,
};
return resp;
}
/**
* Send the jsonify body of the request
* @param {String|Object} body
* @returns Object
*/
function jsonBody(body) {
try {
if (!body) {
return {};
}
if (typeof body === "string") {
return JSON.parse(body);
}
return body;
} catch (e) {
console.error(e);
throw e;
}
}
module.exports = {
response,
jsonBody,
};
Example
libs/dynamodb.js
// ...
getSomething(){
if (!item || item.length === 0) {
throw new ApiError(
"We didn't find what you were looking for.",
'NOTHING_FOUND',
404,
{foo: 'bar'},
"The user request has returned nothing",
);
}
}
// ...
index.js
// Studio Webux S.E.N.C @ 2022
const { response } = require("../libs/response");
const { getSomething } = require("../libs/dynamodb");
const middleware = require("./middleware");
/**
*
* @param {Object} event
* @param {Object} context
*/
exports.handler = async (event, context) => {
try {
// eslint-disable-next-line no-param-reassign
event = middleware(event); // This is the logic to be able to use cognito locally and the one deployed
const {
requestContext: {
authorizer: {
claims: { sub },
},
},
} = event;
if (!event || !event.requestContext.httpMethod) {
return response(400);
}
if (
event.requestContext.httpMethod === "GET" &&
event.requestContext.path.startsWith("/something")
) {
const configurations = await getSomething({ organization: sub });
return response(200, {
data: {
configurations,
count: configurations.length,
},
});
}
// Do your logic here...
return response(501, { message: "Not Implemented" });
} catch (e) {
return response(e.code || 500, { message: e.message, cause: e.cause });
}
};
client.js
VueJS, Vuex code extract to fetch the backend
// ...
async getSomething({ }, something) {
try {
const response = await axios.get(
process.env.VUE_APP_BASE_API_URL + '/something',
{
headers: {
Authorization: (
await Auth.currentAuthenticatedUser()
).signInUserSession.idToken.jwtToken,
},
},
);
console.log(response.data.body.data.message)
return true;
} catch (e) {
if (e.response.data.body.cause === 'SOMETHING_SPECIFIC') {
console.error(e.response.data.body.message)
return true;
}
console.error(e.response.data.body.message)
return false;
}
}
// ...