Introduction
In the ever-evolving realm of software development, errors and exceptions are constant companions. These errors can be broadly categorized into two types: programming errors and operational errors. While programming errors are typically caught during development and testing phases, operational errors are the real-world hurdles that applications must gracefully navigate in production environments. In this blog, we will shine a spotlight on the art of handling operational errors and delve into the pivotal role played by a central error class in Node.js.
Operational Errors: Navigating the Real-World Challenges
Operational errors are a natural part of any software application’s lifecycle. They can arise from various sources, including network issues, database failures, external service unavailability, and even user input. In a typical Node.js application, these errors can occur at different layers, such as the controller, service, and database access (Dao) layers. Handling these errors effectively is crucial for maintaining application reliability and providing a smooth user experience.
The Central Error Class: One for All
In the world of Node.js, it’s a common practice to employ a central error class to manage operational errors efficiently. This central error class serves as a hub for creating, formatting, and handling errors in a structured and consistent manner. By adhering to a single error class throughout your project, you can simplify error management and ensure uniformity in error responses.
For instance, we have MyProjectError
as an central error class.
1 | class MyProjectError extends Error { |
The MyProjectError
class is a custom error class that extends the built-in Error class. It includes additional properties and methods to handle and format errors in a structured way. By using this error class, we are able to create and handle specific types of errors based on their characteristics.
The MyProjectError
class includes properties such as the module in which the error occurred, an error code, a unique identifier for the error, an HTTP status code, causes of the error, and additional details. It also provides methods to add causes to the error and convert the error to a JSON object.
By using this error class, we can create consistent and standardized error objects, making error handling more structured and maintainable.
Utilize Central Error Class
Around the central error class, we are able to utilize the error handling.
1. Unified Error Creation
The central error class allows developers to create and throw specific types of errors with ease. This consistency in error creation facilitates better communication between different layers of your application, ensuring that everyone speaks the same error language.
2. Send Error Responses
When an operational error occurs, it’s essential to communicate the issue to the client in a clear and standardized way. The central error class enables you to generate error responses that adhere to a predefined structure, making it easier for clients to understand and handle errors gracefully.
3. Convert Unknown Errors
Not all errors are predictable, and some may be unexpected or unfamiliar. In such cases, the central error class can help by converting these unknown errors into a generic format that can be handled more effectively. This approach ensures that your application can gracefully recover from unforeseen issues.
4. Localize/Translate Error Messages
In a globalized world, catering to users with diverse language preferences is crucial. The central error class can be extended to include localization and translation features. This allows error messages to be presented to users in their preferred language, enhancing the overall user experience.
For example, we throw the an error when ‘two tenant mode feature is not enabled’.
1 | throw ERRORS.twoTenantModeFeatureNotEnabled(targetTenantUuid, systemAction); |
The error is generated by function twoTenantModeFeatureNotEnabled
1 | twoTenantModeFeatureNotEnabled: (tenantUuid: string, systemAction: SystemAction): MyProjectError |
The function create
is the single entry point to create an error that instances of MyProjectError
. It receives an error code, an error message localization key, a set of parameters for translation, and an HTTP response code.
In the localization properties, we have the definition of errOdc04045
for building an error message.
1 | #YMSE: {0} is the action name of the system |
The error message is built during the serialization phase before sending it to clients.
Error handling in Three Layers of backend service
In a typical backend architecture, your application may consist of three primary layers: the controller, service, and database access (Dao) layers. Each of these layers plays a role in processing requests and handling operational errors.
Controller Layer
The controller layer serves as the entry point for incoming requests and plays a crucial role in error handling and response generation. e.g. we have the entry createSystems
for POST /v1/systems
to create a set of systems in batch as below.
1 | /** |
Error handling in controller layer
In the provided code snippet, we see a global try-catch block wrapping the entry function of the controller. This approach ensures that any exceptions thrown during request processing are caught and handled gracefully. The controller is responsible for crafting and sending responses back to clients. In the provided code, HTTP status codes and appropriate responses are sent, ensuring a consistent and user-friendly experience.
Handling errors isn’t just about catching exceptions; it’s also about providing meaningful error messages to clients. In the provided example, the need for localization of error messages is evident. The error messages need to be translated according to the client’s language preference. To achieve this, a translation search key, such as errOdc04044
, is used to retrieve the correct localized text. This localization information is crucial for delivering a user-centric experience, especially in applications with a global user base.
Service Layer
The service layer is where complex business logic resides. It involves tasks such as data validation, interaction with the database, and building output data. In this layer, error handling is equally critical.
1 | public async createSystems(systemsToCreate: SystemToCreate[]) { |
- Data Validation: Input data received from the controller must be validated and converted to a suitable format for processing. This step helps prevent invalid or malicious data from causing issues further down the line.
- Database Interaction: When interacting with the database, error handling is paramount. Database errors, network issues, or unexpected data inconsistencies can all lead to exceptions. Handling these exceptions gracefully ensures the integrity of the application.
- Output Data: After processing, the service layer is responsible for building and formatting the output data that will be sent back to the controller for response generation.
Error handling in the service layer
The function in the service layer is wrapped in a global try-catch block. In this layer, errors can originate from utilities, helpers, or the database access layer. If an error is not recognized, it should be converted to a generic error.
Database Access Layer (Dao)
The database access layer is responsible for interacting with the database and handling errors that may arise during data retrieval or modification.
1 | private async saveSystemsAndConnections( |
The errors could come from this layer directly. We generate error by a unified entry point and throw it to the caller. We don’t need to cope with the error too much in this layer. However, we need a set of error utilities to generate the error easily.
Conclusion: Streamlining Error Handling for a Reliable Application
In software development, mistakes are not only hurdles but helpful tools for enhancing dependability and user experience. By adhering to recommended methods in managing mistakes, such as sending error responses, transforming unknown errors into a standard format, adopting a unified approach to creating errors, offering error categories, and localizing error messages, developers can build software systems that are more resilient and user-friendly. Embracing mistakes as a part of the development process can result in more robust and reliable applications that serve users better.