Deep dive into try-catch error types in TypeScript

Updated Jul 05, 2023#typescript#how-to

The try...catch statement in TypeScript is similar to the one in JavaScript, which allows you to handle errors that may occur in your code. The try block contains the code that may throw an error, and the catch block contains the code that executes when an error is caught.

try {
  // some code that may throw an error
} catch (error) {
  // some code that handles the error
} finally {
  // some code that will always run
}

The throw statement

The throw statement in JavaScript (and TypeScript) is used to explicitly throw an exception. When an exception is thrown, it disrupts the normal flow of execution and transfers control to the nearest enclosing try...catch statement that can handle the exception.

throw expression;

If no matching catch block is found within the current function, the exception propagates up the call stack to the next enclosing try…catch statement in an outer function. This process continues until a matching catch block is found or until the exception reaches the global scope, resulting in an unhandled exception error.

You can throw any value in TypeScript, including primitive types like number, string, boolean, and symbol. You can also throw objects, arrays, functions, classes, and even custom types like Error or TypeError.

However, throwing values other than Error or its subclasses is generally considered a bad practice, because it makes it harder to handle the errors properly and provide useful information to the user or the developer.

Error type annotations

While TypeScript provides static type checking for most parts of your code, exceptions are considered runtime errors that can occur in various scenarios. Since the type of thrown values is determined at runtime, the TypeScript compiler does not enforce specific types for thrown values.

The error parameter in the catch block represents the error object that was thrown by the try block. The only type annotations that are allowed on catch clause variables are any or unknown, which are the most general types in TypeScript.

Type any is used by default, which means you can access any property or method on it without type checking. However, this can be unsafe and lead to runtime errors if the error object does not have the expected properties or methods.

You can specify the type of the error parameter as unknown, which is a safer alternative to any. This means you have to perform explicit type checking and requires type narrowing before accessing any property or method on the error object.

try {
  // some code that may throw an error
} catch (error: unknown) {
  // some code that handles the error
  if (typeof error === "string") {
    // handle string error
  } else if (error instanceof Error) {
    // handle Error object
  } else {
    // handle other types of errors
  }
}

This option is available since TypeScript 4.0, and you can also enable it by default with the useUnknownInCatchVariables compiler option, then you do not need the additional syntax (: unknown) nor a linter rule to try enforce this behavior.

The built-in Error class

The built-in Error class is a standard class in JavaScript that represents an error object. It has a name property that indicates the type of error, a message property that provides a human-readable description of the error, and a stack property that contains the stack trace of where the error occurred.

The built-in Error class can be used to create and throw custom errors, or to catch and handle errors thrown by other code. For example:

try {
  // some code that may throw an error
  throw new Error("Random error message");
} catch (err) {
  // handle the error
  if (err instanceof Error) {
    console.log(err.name); // the type of error
    console.log(err.message); // the description of the error
    console.log(err.stack); // the stack trace of the error
  } else {
    // handle other errors
  }
}

Standard error classes

The built-in Error class is also used as a base class for other standard error classes, such as SyntaxError, TypeError, ReferenceError, etc. These classes represent different kinds of errors that may occur in JavaScript code, and have their own name and message properties.

  • SyntaxError: Represents a syntax error that occurs while parsing JavaScript code.
  • TypeError: Indicates that an operation or function was performed on an incompatible type.
  • ReferenceError: Occurs when an invalid reference is made to a variable or function that is not defined.
  • RangeError: Indicates that a numeric value is out of the allowed range.
  • URIError: Occurs when encoding or decoding functions such as encodeURIComponent or decodeURIComponent encounter malformed URI strings.
  • EvalError: Deprecated in newer versions of JavaScript, it was previously used to represent errors that occur during the global eval() function.

Create your custom error types

You can create your own error types by extending the built-in Error class or standard error classes. Note that you must call super in the child constructor, and also to set the name property explicitly. Otherwise, the error name will be “Error”, which is not very informative.

class CustomError extends Error {
  constructor(message: string) {
    super(message); // call the parent constructor
    this.name = "CustomError"; // set the name property
  }
}

This way, you can throw and catch instances of CustomError and use the instanceof operator to check their type.

try {
  throw new CustomError("Something went wrong");
} catch (err) {
  if (err instanceof CustomError) {
    console.log(err.name); // CustomError
    console.log(err.message); // Something went wrong
  } else {
    // handle other errors
  }
}

You can also add other properties or methods to your custom error class, such as a code, a data, or a log function.

class HttpError extends Error {
  statusCode: number;

  constructor(statusCode: number, message: string) {
    super(message);
    this.name = "HttpError";
    this.statusCode = statusCode;
  }

  log() {
    console.log(`Http error ${this.statusCode}: ${this.message}`);
  }
}

You can use this class to throw and handle HTTP-related errors:

try {
  throw new HttpError(404, "Not found");
} catch (err) {
  if (err instanceof HttpError) {
    err.log(); // Http error 404: Not found
  } else {
    // handle other errors
  }
}

Custom error types allow you to define meaningful and descriptive error names that convey the specific nature of the error. This improves the clarity and readability of your code, making it easier to understand and maintain.

Best practices for error handling

Handling errors in JavaScript and TypeScript is not much different from other languages. By following these best practices, you can create more reliable maintainable code, and ensure a better user experience when errors occur.

  • Choose error types that accurately represent the nature of the error, such as TypeError, SyntaxError, or custom error types specific to your application domain.
  • Include descriptive error messages that provide useful information about the error.
  • Avoid unhandled exceptions from causing your program to crash.
  • Avoid scenarios where errors are silently ignored or swallowed.
  • Handle exceptions in a way that ensures graceful degradation of functionality and a good user experience. Consider fallback mechanisms or alternative paths to ensure that your program can continue running or recover from errors in a controlled manner.
  • Create a centralized error handling mechanism or utilize frameworks and libraries that provide error handling functionalities. Implement robust logging and monitoring mechanisms to capture and track errors.
  • Test both expected error conditions and the behavior of your code when those errors occur.
  • Leverage TypeScript’s static type checking to catch errors at compile time.