...
JavaScript Error Handling 5 Proven Techniques

JavaScript Error Handling: 5 Proven Techniques

JavaScript error handling may not be glamorous, however, it is perhaps one of the most important aspects of web development. If you have ever sat staring at a blank white screen waiting for an app to load after deployment; or sat watching a user struggle to complete a task because a single failed API request caused the whole system to fail; you probably already know why. JavaScript error handling is simply the safety net that allows your application to continue running even when something unpredictable happens. The idea behind this is to keep a minor glitch from turning into a major disaster.

Think of error handling similar to the crumple zone in a vehicle. You don’t develop the vehicle anticipating that it will crash; however, you engineer it so that should a collision occur, the structural components will absorb the impact and protect those riding within. Likewise, in JavaScript, your application will continue to run, your users will see a friendly error message versus a completely locked up screen; and you’ll be provided with enough detail regarding what went wrong to take corrective action. Therefore, mastering JavaScript error handling simply means incorporating resiliency into every level of your code base.

That said, let us explore five tried and true methods that will revolutionize how you look at JavaScript error handling.

Try…Catch…Finally – The Foundation of JavaScript Error Handling

In JavaScript error handling, try…catch…finally is the foundation upon which all other error handling techniques are based. It is the method used to detect whether a given section of code will potentially cause an issue. If such is the case, the developer wants to be able to react in a manner that minimizes downtime or disruption of services.

From a technical perspective, the basic framework of try…catch…finally is quite straightforward. First, you determine what code you anticipate could result in an error being generated. That is placed within the try block. Should all statements within that block execute successfully, the catch block is bypassed and execution proceeds. However, if any statement results in an exception being generated, the execution flow is instantly redirected to the catch block. At that point, the error object is passed to your code so you can investigate what was incorrect.

Afterward, we have the finally block. Regardless of what occurred prior to reaching this point (i.e., successful execution or an error having been encountered), once the finally block is executed it concludes execution. This makes it ideal for doing clean-up activities (e.g., releasing resources, closing database connections/file handles/etc.).

One of the primary benefits of catch is that the error object passed to you includes various pieces of diagnostic information. Specifically, the message property includes a human-readable explanation of what went awry. Additionally, the name property indicates the nature of the error itself (e.g., TypeError, ReferenceError). Lastly, the stack property lists the sequence of functions called leading up to the error occurring. All three provide invaluable assistance when attempting to pinpoint where things initially began going wrong.

Many developers commit the same error time and again. Rather than properly isolating individual sections of code which are likely to experience errors (such as network requests, JSON parsing, etc.), they opt to create one large try…catch block which encompasses their entire application. On one hand this effectively eliminates the risk of an application-wide failure due to an isolated issue. However, it also masks any errors and severely hampers efforts toward troubleshooting and diagnosing errors when they arise.

Using the correct approach involves encapsulating only those sections of code which are capable of producing errors (network requests, JSON parsing, etc.). By doing so you gain greater control over how each possible failure scenario is addressed. Effectively implementing JavaScript error handling is centered around precision not generality.

Built-in Error Types & Identifying Them

No two errors are alike and knowing the different categories of built-in JavaScript error types will assist you in creating more accurate error recovery plans. JavaScript offers several native constructors which denote particular types of errors.

SyntaxError is essentially the grammar cop. This type of error arises when your code fails to meet syntax requirements defined by the JavaScript programming language (for example, missing closing parentheses or brackets).

Generally speaking, these errors are caught either by a linting program or IDE while your code is being developed. Therefore, generally speaking, you would not normally need to include any form of error handling related to syntax errors in your production code.

ReferenceError represents a situation where you attempt to access a variable which has not been established within the present scope. Essentially this is akin to attempting to withdraw funds from a bank account you never opened.

This is one of the more typical runtime errors you will encounter especially while working with asynchronous coding scenarios since variable scoping can become difficult to manage.

TypeError signifies that you attempted to invoke an operation against a value whose data type does not support such an operation. For instance, invoking a string as if it were a function or accessing a property on “null” or “undefined” will generate a TypeError. These types of errors are commonly experienced when working with APIs which frequently return data shapes that do not align with expectations.

RangeError signals that a numeric value exceeds the acceptable limits for its intended purpose. Creating an array with a negative length or recursively invoking a function until exceeding the maximum number of calls permitted on the call stack will result in a RangeError.

There exist several other less well-known types of errors including URIError (when performing a malformed URI operation), EvalError (when encountering problems associated with use of eval()), and AggregateError (introduced in ES2021 for handling multiple failures resulting from an operation such as Promise.any()).

By employing the instanceof operator to verify the type of error captured within your catch block you can provide more focused fallback behavior when processing TypeErrors vs. NetworkErrors.

Custom Error Classes for Domain Specific Issues

While native error types offer considerable utility, many applications require signaling problems unique to their specific domain. Custom error classes fit neatly into this category and represent one of the more sophisticated tools available in JavaScript error handling.

Defining a custom error is quite simple merely extend JavaScript’s built-in Error class. Define your new class, pass “super(message)” within its constructor to establish the standard error attributes and assign “name” property values that are meaningful for your specific domain. Optionally, you may wish to append additional custom properties containing contextual details concerning your failure.

So why bother defining custom errors as opposed to passing a plain Error with a descriptive message? Precision. When you throw a ValidationError or DatabaseError, your catch blocks can employ instanceof checks to determine exactly what type of problem has arisen and respond appropriately. This is especially true as your application grows larger and requires different recovery approaches for different failure cases.

An example might be an asynchronous readUser() function responsible for reading user data from JSON files. You may choose to throw a SyntaxError if the JSON file is malformed. Alternatively, you may decide to throw a custom ValidationError if although syntactically valid, the JSON file omitted fields essential for identifying users (e.g., email address). In turn, your catch block(s) can utilize instanceof checks to differentiate between instances of ValidationError and catch other types of errors; thereby displaying an appropriate message to users while returning generic messages for other types.

Handling Asynchronous Errors Using Async / Await

Asynchronous coding presents an obstacle relative to JavaScript error handling. If you generate an error in the middle of setTimeout() callback or within a promise chain and do not capture it using a try/catch block then that error will not be caught in any surrounding try/catch block. Why? Because by that time try/catch had completed executing long ago while your asynchronous operation continued.

Async/await simplifies this problem dramatically. When you declare an async function any error produced within that function becomes a rejected promise. Any promise awaiting another promise within a try block will automatically direct execution into the catch block should any part of that awaited promise reject. This allows you to apply the identical familiar try/catch paradigm for both synchronous and asynchronous error detection.

However, there exists one subtle aspect worthy of note. If you wish to execute multiple await calls concurrently in order to improve performance; wrapping each separately in their own try/catch could quickly become unwieldy. One strategy is to encapsulate the entirety of concurrent execution within a single try/catch block though this complicates distinguishing specifically which operation resulted in failure. A cleaner alternative would be applying Promise.allSettled() which awaits completion for all promises presented and returns a detailed report detailing both successes and failures without ever causing rejection.

Amongst the most frequent pitfalls with respect to asynchronous error management lies the fact that fetch() does not reject based upon HTTP status codes indicating errors such as 404 or 500. It only rejects based upon network-related failures. Therefore, you still need to programmatically examine response.ok and generate an error yourself should any status code indicate an unsuccessful response regardless of whether that occurs due to network connectivity loss or server-side resource availability issues. This is simply good defensive programming practices related to JavaScript error handling.

Global error handling and unhandled rejections

Although no matter how well planned out your try catch block may be there will always be errors that slip through. This is where global error handlers come into play. These are your last line of defense, catching any unhandled exceptions and rejected promises and stopping your application from breaking silently.

If you cannot identify where your synchronous errors are being generated that are escaping every applicable try/catch block you can attach an onerror handler to window.onerror. When attached any time an uncaught exception occurs the onerror event will trigger passing along information about the error message, source file url, line number and column number giving you enough information to use either internal/external reporting mechanisms or present fallback UIs to the end user.

If you would like to capture any asynchronous promise rejections that are never caught with a .catch handler you can subscribe to the unhandledrejection events that get fired on the windows object. The process of subscribing to these events is very similar to attaching an onerror handler. If any promise rejects without a .catch handler attached the unhandledrejection event will fire providing you with detailed information regarding the failing promise, including the specifics of why it was rejected. Assuming that unhandledrejection events have been enabled in modern browsers; otherwise browsers have a default behavior to automatically log any unhandledrejections giving you the ability to add your own logging detail or override the default browser behavior.

1. Note: calling ‘true’ within a global error handler (global) will stop the default browser behavior of showing an error dialog to the end user. Utilizing this functionality is beneficial when implementing a fully customized error reporting mechanism. However utilizing this functionality too heavily could mask bugs that you want to see.

2. Global error handling does not supplant local try…catch blocks. Local try…catch blocks allow you to handle errors as close to the actual occurrence as possible, enabling you to diagnose and correct them. Global error handlers offer a safety net in case some errors are not handled locally, then you will at least know that something went wrong.

Conclusion

In conclusion, mastering JavaScript error handling will create a difference between robust applications and fragile ones. In addition to learning the basic try…catch…finally syntax, we have also learned the main Error class, how to extend Errors with custom error classes to represent errors specific to your application or business domain, async error handling, and how to use global error handlers to catch any remaining errors.

By learning and mastering these five concepts, you will greatly improve your way of thinking about error handling in your applications. If you would like to learn more about different ways of implementing error handling patterns and the official JavaScript documentation for Control Flow and Error Handling. Additionally, if you would like to read a more detailed explanation of how to build custom error hierarchies.

New to HTML? Start Here: HTML Tutorial for Beginners: Your Complete Introduction to HTML Basics
New to CSS? Start Here: CSS Introduction: Master 5 Core Concepts Easily
New to JavaScript? JavaScript Introduction: 5 Proven Steps to Learn JS

[INSERT_ELEMENTOR id=”122″]

Leave a Comment

Your email address will not be published. Required fields are marked *

Seraphinite AcceleratorOptimized by Seraphinite Accelerator
Turns on site high speed to be attractive for people and search engines.