Webux Lab

By Studio Webux

Extend Error handling with NodeJS

TG
Tommy Gingras Studio Webux 2022-08-13

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;
    }
  }
// ...

Search