How to Handle Errors in JavaScript
Handling errors or exceptions is an integral part of software application development. Errors or exceptions can cause the program to work in an unexpected manner, resulting in security issues or inconvenience to the user. For this reason, it is very critical to handle errors appropriately.
Let’s consider a simple software program like a calculator performing division of two numbers. The software provides an input screen for users to enter the data on which the operation is required to be performed. Now, what if the user types in an invalid input, like say text instead of numbers or tries to perform division by zero. What happens if an error occurs?
Any software is designed to perform orderly transitions between different states. Now because of errors, we might end up being in a state which is not considered or envisaged during software development. This situation is known as unpredictable state of the software and can lead to serious vulnerabilities such as loss of data, buffer overflows etc.
Error handling, also called as exception handling, will help us prevent this unpredictable state by providing explicit instructions on handling unpredictable states. For example, if the user enters string data instead of numeric data for performing division operation, we can validate the data by transitioning to validation state for checking conditions that might create an error. If there is no error, software would continue to execute, otherwise it would enter a state where it would display the error message.
Like most programming languages, JavaScript also has exception handling statements i.e. throw statement and try…catch statement.
A note on exception Types: JavaScript provides predefined exceptions for specific purposes and it is suggested to use one of them for effectiveness. That said, any object can be thrown in JavaScript and the most common practice is to throw numbers or strings as errors.
throw statement is used to throw an exception with a specified value to be thrown. A few examples are as follows:
try…catch statement consists of a try block which contains multiple statements and a catch block containing the statements to be executed if an exception happens in try block during execution of statements in that block.
In a positive flow, all the statements in try block succeed and control will skip the catch block. Otherwise, if there is an exception thrown in the try block, the following lines in try block are skipped and the control shifts to catch. In either case, finally block executes after try and catch blocks are executed.
In catch block, we have an identifier that holds the value specified by the throw statement. This can be used to get the information about the exception details. The scope of this identifier lasts within catch block and post finishing the execution, the identifier no longer exists.
Note on best practices: When logging errors to the console inside a catch block, it is advisable to use console.error() rather than console.log() for debugging. It formats the message as an error, and adds it to the list of error messages generated by the page.
Few pitfalls or best practices to be known while considering finally block
Nesting of try..catch statements is possible; but if inner try block does not have corresponding catch block then it should have a finally block. The enclosing try…catch statement’s catch block is checked for a match.
Error objects are a specific type of core objects which are thrown during runtime. To log more refined messages about error, we can use ‘name’ and ‘message’ properties.
Refer to this link to know more about Error object
For example, throw new Error(‘Error Message’); when caught in catch block, the identifier will have the property name as ‘Error’ and property message as ‘Error Message’.
When a runtime error occurs, error objects are created and thrown. Any user-defined exceptions can be built extending the Error.
Below are some of the predefined error constructors extended from generic Error constructor.
Let’s understand each one with an example.
1. AggregateError as the name suggests is used to wrap multiple errors into a single error. Imagine we are processing multiple async calls via promises and used Promise.any(). This would raise AggregateError or we can create our own new AggregateError as shown below.
OR
2. EvalError occurs when using global eval() function. This exception is no more thrown by JavaScript.
3. RangeError is to be thrown when a value is not in the range of allowed values. Like passing bad values to numeric methods like toFixed, toPrecision etc.
4. ReferenceError is raised when a non-existent variable is referenced.
5. SyntaxError is thrown by the JavaScript engine if it encounters tokens which do not conform to the syntax of the language while parsing the code.
6. TypeError is raised if an operation is performed on a value which is not expected on its type. Say if we are attempting to modify a value that cannot be changed.
7. URIError is raised when global URL handling function is used in an inappropriate way.
If we want to handle different error types differently then we can check for error instanceof and handle them accordingly.
Note: In real time applications, we would handle specific error types based on the functionality in the try block and wouldn’t have so many different error types for a single try…catch block. Also having multiple errors and having multiple if … else statements is not recommended as per the single responsibility principle.
Please refer to the above code only for reference to identify the specific error type.
Sometimes we would like customize the error or want to have our own error types. To create custom Error types, we have to extend the existing Error object and throw the custom error accordingly. We can capture our custom error type using instanceof. This is a cleaner and more consistent way of error handling.
The DOMException occurs as a result of calling a method or accessing a property of a web API which represents an abnormal event that occurred.
Refer to this link to know more about DOMException
A Promise is an object representing the eventual completion or failure of an asynchronous operation. Promises allow to chain multiple asynchronous operations back-to-back in a sequence order.
When we chain multiple asynchronous operations, then we can have catch at the end of the chain to capture any exception or rejection that happened at any promise. Catch takes a callback function and the callback is set to execute when the Promise is rejected.
For example:
As per the above example, we are performing multiple async actions but we have a single catch block to capture any rejection. Let’s understand this with an assumption that the calls are synchronous in nature.
If there’s an exception, then the browser will jump over to catch block and execute the failureCallback.
Based on ECMAScript 2017 async/await syntactic sugar-coated way, we can change the synchronous code to asynchronous.
Promises help to resolve the callback hell by catching all errors i.e. thrown exceptions or programming errors. This is important for functional composition of async operations.
Promises are associated with two events i.e. rejectionhandled and unhandledrejection. Whenever a promise is rejected, one of these two events is sent to the global scope which might be a window or a worker.
Note: Both events are of type PromiseRejectionEvent which has details of the actual promise which was rejected and a reason property depicting the reason for rejection.
Since both the events are in global scope, all errors will go to the same event handlers regardless of the source. This makes it easy to offer a fallback error handling mechanism for promises.
While working with Node.js on the server side, we might include some common modules and there is a high possibility that we may have some unhandled rejected promises which get logged to the console by the Node.js runtime. If we want to capture those and process or log them outside the console, then we can add these handlers on the process as shown below.
The impact of adding this handler on the process is that it will prevent the errors from being logged to the console by the node.js runtime. There is no need for calling preventDefault() method available on browser runtimes.
Note: Adding the listener on the process.on and not coding to capture the promise and reasons would result in loss of the error. So it is suggested to add code to examine the rejected promise to identify the cause of rejection.
The global scope being window (may also be worker), it is required to call the event.preventDefault() in the unhandledrejection to cancel the event. This prevents it from bubbling up to be handled by the runtime’s logging code. It works because unhandledrejection is cancellable.
To summarise a few points related to error handling:
Conclusion
Error handling is one of the most important facets in software programming that has to be acknowledged and experienced by the developer. Software should be as predictable as possible in terms of state transitions.
Research & References of How to Handle Errors in JavaScript|A&C Accounting And Tax Services
Source