1. JavaScript
  2. Fundamentals
  3. Exception Handling

JavaScript Exception Handling

Last updated:

The process of identifying, catching, and responding to errors that occur during the execution of a JavaScript program. Exceptions are errors that are thrown when something unexpected happens in the code - handling them prevents the program from crashing or behaving unpredictably.

Types of Exceptions

  • Syntax errors - occur when the code violates the syntax rules of the language and are detected by the JavaScript engine during the parsing phase.
  • Runtime errors - occur during program execution when an operation cannot be completed - can be more challenging to detect than syntax errors because they do not cause the program to crash immediately but unexpected behavior.

Throw Exception

An exception can be thrown manually using the throw statement, with the option to create a custom error message or to throw a predefined error object.

function divide(x, y) {
    if (y == 0) {
        throw new Error("Cannot divide by zero");
    }
    return x / y;
}

The try-catch Statement

try {
    // code that might throw an exception
} catch (error) {
    // code that handles the exception
}
`

If an exception is thrown in the try block, the JavaScript engine jumps to the catch block, which handles the exception.

```javascript
function divide(x, y) {
    if (y == 0) {
        throw new Error("Cannot divide by zero");
    }
    return x / y;
}

try {
    console.log(divide(42, 0));
} catch (error) {
    console.log(error.message);  // prints: "Cannot divide by zero"
}

Catching Specific Exceptions

Handle different types of exceptions differently by using multiple catch blocks or by using the instanceof operator.

try {
    // code that might throw an exception
} catch (error) {
    if (error instanceof TypeError) {
        // code that handles TypeError
    } else if (error instanceof RangeError) {
        // Code to handle RangeError
    } else {
        // code that handles other exceptions
    }
}

Finally block

try {
    // Code that may throw an exception
} catch (error) {
    // Code to handle the exception
} finally {
    // Code executed regardless of whether an exception is thrown or not
}

NB: The finally block is optional and can be used to perform cleanup tasks, such as closing a file or releasing a resource.

const file = openFile("rates.csv");

try {
    // Code that reads from or writes to the file
} catch (error) {
    // Code to handle the exception
} finally {
    closeFile(file); // Release the file resource
}

Error Objects

Specialised objects that encapsulate information about an error that occurs during the execution of a script. They provide essential details about the error, such as a message describing the error, the type of error, and the location of the error in the source code. JavaScript has a built-in Error object, which serves as the base object for all error types.

Error objects have two primary properties:

  • name - refers to the error’s type.
  • message - contains a human-readable description of the error.

The Error object also has a stack property, which returns a string representing the stack trace at the time the error was thrown.

function divide(a, b) {
    if (b === 0) {
      throw new Error("Division by zero");
    }
    return a / b;
}

function quotient(a, b) {
    return divide(a, b);
}

try {
    quotient(42, 0);
} catch (error) {
    console.error("Error message:", error.message);
     // prints: "Error message: Division by zero"

    console.error("Error stack trace:", error.stack);
    /* prints:
      Error stack trace: Error: Division by zero
        at divide (REPL24:3:11)
        at quotient (REPL28:2:10)
        at REPL35:2:3
    */
}

Custom Error Objects

Extend the Error Object

Custom error will inherit the properties and methods of the base Error object, allowing for the creation of more specific error types tailored to current needs.

class CustomError extends Error {
  constructor(message) {
    super(message);
    this.name = 'CustomError';
  }
}

Error.prototype

Extend the Error object by creating a custom constructor function. The constructor function should accept an error message as an argument and call the Error object’s constructor with the message. This way, the new error object will have all the properties of the built-in Error object, along with any additional properties specified by the custom constructor function.

function CustomError(message) {
  this.name = 'CustomError';
  this.message = message || 'Default error message';
  this.stack = (new Error()).stack;
}

CustomError.prototype = Object.create(Error.prototype);
CustomError.prototype.constructor = CustomError;

Built-in Error Objects

ReferenceError

Triggered when attempting to use a variable that has not been defined or is not in the current scope:

function greet() {
  try {
    console.log("Hello " + name);
  } catch (error) {
    if (error instanceof ReferenceError) {
      console.error("Error:", error.message);
    } else {
      // any other error types
      console.error("Unexpected error:", error.message);
    }
  }
}

greet();  // prints: "Error: name is not defined"

TypeError

Triggered when attempting to perform an operation on a value of an incorrect type, such as calling a non-function as a function or accessing a property on a non-object:

const greeting = {
  hello: function () {
    return "Hello!";
  },
};

try {
    // prints: hello
    console.log(greeting["hello"]());
    // prints: TypeError: greeting.goodbye is not a function
    console.log(greeting["goodbye"]());
} catch (error) {
    if (error instanceof TypeError) {
        console.error("TypeError:", error.message);
    } else {
        // any other error types
        console.error("Unexpected error:", error.message);
    }
}

RangeError

Triggered when providing a value that is outside the allowed range for a particular operation or data structure:

function createArray(size) {
    try {
        return new Array(size);
    } catch (error) {
        if (error instanceof RangeError) {
            console.error("RangeError:", error.message);
        } else {
            // any other error types
            console.error("Unexpected error:", error.message);
        }
    }
}

// prints: RangeError: Invalid array length
createArray(-3);

SyntaxError

Triggered when there is an issue with the syntax of the code, typically during parsing or evaluation - a common occurence is when using eval():

function execute(code) {
    try {
        return eval(code);
    } catch (error) {
        if (error instanceof SyntaxError) {
            console.error("SyntaxError:", error.message);
        } else {
            // any other error types
            console.error("Unexpected error:", error.message);
        }
    }
}

// prints: "Hello, World!"
execute("console.log('Hello, World!');");

// prints: "SyntaxError: missing ) after argument list"
execute("console.log('Hello, World!';");

EvalError

This is a deprecated error object related to the global eval() function - it is not thrown by the current ECMAScript specification with other error types such as SyntaxError, ReferenceError, or TypeError are thrown when issues encountered.

NB: The use of eval() is generally discouraged in modern JavaScript programming, as it can lead to security and performance issues.

URIError

Thrown when a malformed URI is provided to certain global functions, such as encodeURI(), decodeURI(), encodeURIComponent(), or decodeURIComponent():

function decode(encodedURI) {
    try {
        return decodeURIComponent(encodedURI);
    } catch (error) {
        if (error instanceof URIError) {
            console.error("URIError:", error.message);
            return;
        }
        // any other error types
        console.error("Unexpected error:", error.message);
    }
}

// prints: "://example.com/test"
decode("%3A%2F%2Fexample.com/test");

// prints: "URIError: URI malformed"
decode("%C0%AFexample.com/test%");

Best Practices

  • always throw an instance of an error object instead of a plain string or number to ensure proper error handling.
  • use meaningful error messages: they should be descriptive and provide enough information to help developers identify the cause of the exception.
  • use appropriate built-in error types when throwing errors. E.g. throw a ReferenceError when encountering an issue with an undefined variable.
  • create custom error objects for application-specific errors.
  • handle at the appropriate level: Exceptions should be handled at the appropriate level of the application. For example, if an exception occurs in a function, it should be caught and handled in that function rather than higher up in the call stack.
  • avoid catching generic exceptions: Catching generic exceptions can make it difficult to identify the cause of the exception.
  • use the finally block for cleanup: The finally block should be used for cleanup operations, such as releasing resources or closing database connections.
  • utilize the ‘stack’ property for better debugging, especially in development environments.
  • handle errors gracefully by providing fallback behavior or informative messages for users.