By combining custom errors, named functions, and Bugfender, you can create a robust JavaScript exception-handling process that allows you to immediately identify the defects of your JavaScript apps. This article shows you some strategies, approaches, and best practices to ensure you create high-quality and high-performing JavaScript applications.
Table of Contents
- Why do we need to handle exceptions in JavaScript?
- What causes unhandled exception errors?
- JavaScript Exception handling
- Throwing custom JavaScript exceptions
- Understanding the Stack Trace
- Global Error Handling
- Most common JavaScript exceptions
- Using a JavaScript Exception Monitoring and Reporting Tool
- JavaScript Exceptions FAQ
- Summary
Why do we need to handle exceptions in JavaScript?
Unhandled JavaScript exceptions will stop the execution of your script, leaving the application in an undesired state – or, even worse, in an unknown state. So you need a robust exception-handling process to avoid unknown errors in your apps.
But, why are exceptions thrown anyway? No one wants their application to end up in a broken state. Wouldn’t it be easy if JavaScript exceptions were just not thrown at all?
What causes unhandled exception errors?
The JavaScript Runtime environment throws errors to let the developers know that something went wrong. Common examples of exceptions are:
- Syntax Error when invalid source code is written.
- Type Error when an argument is not of an expected type.
- Internal Error when the JavaScript engine can’t continue with the execution (for example “too much recursion” error).
In general, you should think about thrown exceptions as an opportunity to fix whatever is causing them, and to improve your product.
From the developer perspective, it is even good practice to deliberately throw errors when some conditions for the correct functioning of the application do not match.
function div(x, y) {
if (y <= 0) {
throw "0 is not allowed";
}
return x / y;
}
console.log(div(2, 3));
console.log(div(3, 0));
JavaScript Exception handling
As said, it is good practice to throw exceptions as often as needed. However, these exceptions need to be caught and controlled before they arrive to the user. That’s why we need to have a good exception handling strategy.
Handling exceptions allows the developer to take control of the flow after something bad has happened, and it redirects the app flow to a new stable state. This new state could consist of silently redirecting the user to another acceptable state without them even noticing, informing the user that an error has occurred, safely logging the error for further debugging… or all of those things together.
Where are JavaScript exceptions logged?
JavaScript exceptions are typically logged in the console. If the application is a website, you can visualize all the logs by opening the browser’s console within the inspection tool.
If it is a node.js
or backend environment, you can see the exception logs in the terminal.
Specifically for websites, this is only useful during development, because you cannot see the browser console on your user’s computer. But you can monitor exceptions that occur in the final users’ environment, using an external logging tool like Bugfender.
JavaScript exception handling using a try/catch block
As is the case with many other languages, you can enclose dangerous JavaScript code inside a try/catch/finally
block. When an error is thrown, your program execution falls back to the catch
block. Here, in the catch
statement, is where you can take action on any runtime error that has been thrown and take corrective action if needed.
The finally
block is an optional block that you can add and it will be executed regardless of whether the try
block ended up in error or not.
Notice that unlike other programming languages, JavaScript does not allow for errors to be catched by error type. Instead, you should use a conditional catch block.
try {
myroutine(); // may throw three types of exceptions
} catch (e) {
if (e instanceof TypeError) {
// statements to handle TypeError exceptions
} else if (e instanceof RangeError) {
// statements to handle RangeError exceptions
} else if (e instanceof EvalError) {
// statements to handle EvalError exceptions
} else {
// statements to handle any unspecified exceptions
logMyErrors(e); // pass exception object to error handler
}
}
// Source : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch
Understanding the JavaScript Error object
When you use a try catch block in your code, in the catch block, you will likely receive a JavaScript Error object. This object is designed to provide a representation of errors with a message attribute. Learning about this object will enable you to improve your debugging process and be able to fix bugs in the code more easily.
The Error object has the following properties:
message
: A string describing the error detail.name
: Specifies the error name or type.stack
: Provides a stack trace that indicates where the error occurred (might not be supported in some old browsers)
A common practice, as you will see in the next section, is to create your custom Error objects that extends the base JavaScript Error object.
Exception Propagation in JavaScript Async/Await
When you are writing asynchronous code in JavaScript, it’s important to understand how exception propagation works. In the case of JavaScript async code, any error that occurs in an async function will be propagated to the await call. This makes exception handling in asynchronous JavaScript very intuitive and straightforward, as it’s similar to traditional synchronous exception handling practices.
Let’s see an example of how this works:
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return await response.json();
} catch (error) {
console.error('There was a problem with the fetch operation:', error);
throw error; // Re-throw the error to be caught by the caller
}
}
async function getData() {
try {
const data = await fetchData();
console.log('Data received:', data);
} catch (error) {
console.error('Error fetching data:', error);
}
}
In this example, if fetchData()
fails and throws an exception, the catch block catches and handles it, allowing for graceful error handling and response.
Throwing custom JavaScript exceptions
JavaScript provides a method to generate user-defined exceptions. To generate a custom JavaScript exception you can use the throw
statement. In fact it’s common practice to use this statement and include numbers or strings as an identifier or as status code.
function getRectArea(width, height) {
if (isNaN(width) || isNaN(height)) {
throw 'Parameter is not a number!';
}
}
When it comes to application-specific errors, a more effective alternative is to extend the generic class and create a custom exception. With a custom error object, you can create your own error code and error message. Here you have a very basic example of how to create a custom error:
function MyError(message) {
const error = new Error(message);
return error;
}
MyError.prototype = Object.create(Error.prototype);
throw MyError("this is my error");
A custom exception and error object is handy for:
- Application-specific errors: errors that have meaning only in the context of your application. For example a
ProductNotFoundError
message in a marketplace. The application-specific errors help you to quickly catch your app defects. - Extending generic classes and providing detailed information: for example using custom properties. A custom
HttpNotFoundError
might include a custom field that helps you to find the reason for this error. In this specific case, it may be useful to store the URL that wasn’t found and the date for further debugging. - Overall, better clarity. Creating custom exceptions consists of somehow identifying and classifying the errors that your app can have. It will help you gain a better understanding of your weak points and speed up the development of your software.
In the next code example, you can see a more advanced creation of a custom error class:
class MyNotFoundError extends Error {
constructor(message = '', url = '', ...params) {
super(...params)
this.name = 'MyNotFoundError'
// Custom error data
this.message = message;
this.url = url;
this.date = new Date()
}
}
try {
throw new MyNotFoundError('Page not found', 'http://mynotfoundurl.com');
} catch (e)
{
console.log(e.name);
console.log(e.message);
console.log(e.url);
console.log(e.date);
}
As you can see in the previous example, by creating a custom error object and adding a proper error handler, we can get all the information related to the error and print our own error message to the console, so we can fix it easily when it happens later. This is especially useful if your application has a log collector tool like Bugfender, so you can see all your user errors in a single place.
Understanding the Stack Trace
The stack trace is the path in the code that the JavaScript runtime has executed up to and until a specific point. That point can be an error, a breakpoint or a console.trace
command.
By examining the stack trace, developers can trace the flow of their code and identify the specific functions, methods, or lines of code that guided to the error. This knowledge helps debugging to become a more efficient process, as developers can focus their attention on the relevant sections of code to fix the issue.
When analyzing a stack trace, it’s important to take into consideration the difference between named and anonymous functions.
Anonymous functions VS named functions
An anonymous function is pretty much what you might think: a function without a name.
var x = function (a, b) {return a * b};
They have a few benefits, like accessing a wider scope and inline declaration. This adds clarity to your code when, for example, you are implementing a callback in the exact place that will be called.
On the other hand, a named function looks like this:
function myFunction(a, b) {
return a * b;
}
And the main benefit is that myFunction
name can be logged and captured by the stack trace.
With the previous example of throwing a custom error, you can see what a stack trace in JavaScript looks like:
As you can see, if you use named functions it is very easy to follow the execution of your code until you reach an error.
Global Error Handling
If you are building a browser JavaScript application, it might happen that, even after taking a lot of care to handle all the exceptions in your code, there’s an unexpected error somewhere in your code or a library you use that breaks your app. To avoid this problem, it is recommended to have a global error handling mechanism in place.
Global error handling in JavaScript can be implemented using the window.onerror
event listener. This method captures all uncaught errors that happen in the browser, providing a way to log these errors or respond to them systematically. Here’s a basic example:
window.onerror = function(message, source, lineno, colno, error) {
console.log('An error occurred:', message, 'at line:', lineno, 'in file:', source);
// Prevent default handling of the error
return true;
};
This function gets called whenever an uncaught error occurs, giving you the error message, source file, line number, column number, and the Error object itself. It provides a centralized way to handle errors that weren’t caught by try/catch blocks or promise rejections.
Most common JavaScript exceptions
The most common JavaScript exceptions are errors that occur during the execution of a JavaScript code. When the JavaScript interpreter find an error in code’s syntax or on the execution of the code it will throw an exception. These exceptions can halt the execution of the script if they are not properly handled using try...catch
blocks. Here’s a list of some of the most common JavaScript exceptions.
It’s important to understand that as JavaScript is an interpreted language, any syntax error in your code will generate an exception in runtime, compared to complied languages where the error will be visible during the compilation phase. That’s why having an exception handling in place it’s very important for JavaScript applications.
JavaScript ReferenceError
A ReferenceError
exception is thrown when you try to access a variable that hasn’t been declared yet. This can also happen if you try to to access a property on an undefined object. It means that the code is trying to access to something that the JavaScript engine can’t find in the current scope or in the global scope.
ReferenceError: variable is not defined
This happens when you try to use a variable that has not been declared anywhere in the code.
try {
console.log(x);
} catch (e) {
console.error(e); // ReferenceError: x is not defined
}
ReferenceError: Cannot access ‘variable’ before initialization
When you are trying to access or modify a variable before it’s declared with let
or const
. This is specific to let
and const
, as variables declared with var
are hoisted.
try {
console.log("The value of x is: " + x);
let x = 5;
} catch (e) {
console.error(e); // ReferenceError: Cannot access 'x' before initialization
}
JavaScript TypeError
A TypeError
exception is thrown when you try to do an operation on a value of an incompatible type. This can happen because you are doing operation between different types of variables or trying to access invalid properties.
TypeError: function is not a function
When you are trying to call something that is not a function. This often occurs when the variable intended to be a function is undefined
or has been assigned a non-function type.
var someFunction;
try {
someFunction();
} catch (e) {
console.error(e); // TypeError: someFunction is not a function
}
TypeError: Cannot read properties of undefined
You will get this JavaScript exception when you try to access a property or method that don’t exist in the object. This is a very common mistake, especially when dealing with objects or data that may not be initialized.
var obj;
try {
console.log(obj.someProperty);
} catch (e) {
console.error(e); // TypeError: Cannot read property 'someProperty' of undefined
}
JavaScript RangeError
A RangeError
exception is thrown when a value exceed the allowed range. This exception is very common when you are dealing with arrays and not properly checking sizes in your code or with variables containing dates.
Be aware that JavaScript doesn’t throw an exception if you access an array element outside its bound, instead it will return an
undefined
value. If you want to check these kind of errors, you need to check it in your code.
RangeError: Invalid array length
It happens when you try to create an array with a length that is not a positive integer within the acceptable range (0 to 2^32-1). The following code it’s quite simple and easy to avoid, but when the array length comes from complex calculations, you need to make sure that the size is within the valid range with a conditional.
try {
var arr = new Array(-1);
} catch (e) {
console.error(e); // RangeError: Invalid array length
}
RangeError: Maximum call stack size exceeded
When using recursion in your code base, it’s very common to forget the base case that terminates the recursion. If you don’t proper terminate it, you will end exceeding the maximum call stack size and get an exception.
function recursiveFunction() {
try {
recursiveFunction();
} catch (e) {
console.error(e); // RangeError: Maximum call stack size exceeded
}
}
recursiveFunction();
RangeError: Invalid time value
If you create a Date object with dates outside the valid values for years, months, days, etc., it can lead to a RangeError
exception. In this case, the exceptions depends on the browser implementation and are not consistent between them.
try {
var date = new Date("2014-25-23")
date.toISOString();
} catch (e) {
console.error(e); // RangeError: Invalid time value
}
Using a JavaScript Exception Monitoring and Reporting Tool
Adding an Exception Reporting Service is the last step to improve your app and make it more robust. This tools capture JavaScript errors in real-time, so you can track and analyze the behavior of you application and quickly identify, diagnose, and fix errors before they impact users
Bugfender is one of this tools that will help you with that. If you integrate Bugfender into your app you can log anything you might need to identify exactly what happened later. And you will also get all the information on any unhandled JavaScript errors.
Once you receive a user report, head to the Bugfender console and find exactly what happened, as well as the context of the error.
If you don’t have a Bugfender account yet, you can follow our post with the different ways to use Bugfender in a web app or just get a free account right here.
JavaScript Exceptions FAQ
Why is exception handling important in JavaScript?
Exception handling in JavaScript is crucial to prevent scripts from stopping and leaving the application in an undesired state. It ensures app stability and performance.
What common errors require handling in JavaScript?
Syntax errors, type errors, and internal errors like “too much recursion” are common errors that require handling in JavaScript.
How can JavaScript exception be logged?
JavaScript exceptions are typically logged in the console for websites and in the terminal for node.js or backend environments.
What is a try/catch block in JavaScript?
A try/catch block in JavaScript allows developers to catch and manage errors thrown during the execution of code blocks, ensuring errors do not negatively impact the user experience.
Why should custom JavaScript exceptions be thrown?
Throwing custom JavaScript exceptions helps in identifying and handling application-specific errors more effectively, allowing for clearer error messages and debugging.
What advantages do named functions offer in exception handling?
Named functions enhance exception tracking by appearing in stack traces, making it easier to diagnose and fix errors.
Summary
In summary, use a bit of extra time during your development to put in place a proper exception handling. It’s important to combine custom errors with named functions and the integration of an Exception Reporting tool like Bugfender. With all of these things, you will be mastering exception handling and building a very robust app that you can easily debug and fix.
Remember, less time looking for errors means more time to build new features.
Happy debugging!