Skip to main content

Error handling is a fundamental aspect of any programming language. It ensures that your code runs smoothly, handles unforeseen errors gracefully, and provides informative feedback to users or developers. In JavaScript and TypeScript, error handling is done primarily using try...catch blocks, and TypeScript offers enhanced safety by introducing static typing. Let’s dive deep into how error handling works in these languages and how you can use it effectively in your code.


1. Why Error Handling is Important

Errors are inevitable in programming. They can arise from user inputs, failed network requests, or bugs in the code. Without proper error handling, these errors can cause your application to crash, leading to a poor user experience and potentially causing data loss. The main objectives of error handling are:

  • Graceful Failure: Ensure the application doesn’t crash unexpectedly.
  • Debugging Aid: Provide helpful messages to developers to identify issues.
  • User Feedback: Offer meaningful messages to users in case of an error.

2. Types of Errors in JavaScript/TypeScript

Before jumping into error handling techniques, it’s essential to understand the types of errors you might encounter:

  • Syntax Errors: These occur when the code doesn’t follow the correct syntax rules of the language.
  • Runtime Errors: These occur during code execution due to unforeseen problems, such as a reference to an undefined variable.
  • Logical Errors: These are bugs where the code runs, but the output is not as expected.

3. JavaScript Error Handling with try...catch

The try...catch block in JavaScript allows you to handle runtime errors gracefully:

try {
  // Code that may throw an error
  const result = someFunction();
  console.log(result);
} catch (error) {
  // Handle the error
  console.error("An error occurred: ", error);
} finally {
  // Optional block that always executes, even if there's an error
  console.log("This will always run");
}

How It Works:

  • The code inside the try block is executed first.
  • If an error occurs, JavaScript stops executing the code in the try block and jumps to the catch block.
  • The catch block receives the error object, which can be logged or handled.
  • The finally block (if present) will always execute, whether an error was thrown or not.

Common Patterns:

  1. Throwing Custom Errors: You can throw your own custom error using the throw statement:
function validateAge(age) {
  if (age < 18) {
    throw new Error("Age must be 18 or older.");
  }
}

try {
  validateAge(16);
} catch (error) {
  console.error(error.message); // Age must be 18 or older.
}
  1. Using finally for Cleanup: The finally block is often used to perform cleanup tasks:
try {
  openDatabaseConnection();
  // Do something with the connection
} catch (error) {
  console.error("Error while accessing the database.");
} finally {
  closeDatabaseConnection(); // Always close the connection
}

4. Enhancing Error Handling with TypeScript

TypeScript brings the benefit of static type checking, which helps prevent many runtime errors. TypeScript’s strong typing system allows you to catch errors during development before they ever make it to runtime.

Type Safety

One of the primary benefits TypeScript offers is the ability to define the types of variables, function arguments, and return types, minimizing errors:

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

try {
  const result = divide(10, 0);
} catch (error) {
  console.error(error.message); // Division by zero
}

In this example, TypeScript ensures that a and b are numbers at compile time, so there’s no risk of passing incompatible types to the divide function.


5. Using unknown and any for Error Handling

In TypeScript, the catch block accepts an error of type any. This is problematic because you lose type safety, and handling the error effectively can become tricky. Instead, you can explicitly specify the error type as unknown and then narrow it down inside the block:

try {
  throw "This is an error";
} catch (error: unknown) {
  if (typeof error === "string") {
    console.log("Error: ", error);
  } else {
    console.log("Unknown error");
  }
}

The unknown type forces you to check the type before accessing properties, ensuring that you handle errors more safely.


6. Best Practices for Error Handling

Here are some tips to ensure that your error handling is efficient and effective:

  • Don’t Ignore Errors: Always handle errors, even if it’s just to log them.
  • Use Custom Errors: Create meaningful custom error types that describe specific issues in your application.
  • Log Errors Appropriately: In development, log errors with stack traces. In production, avoid logging sensitive information.
  • Graceful Degradation: If an error is non-critical, consider providing fallback functionality instead of crashing the entire application.
  • Use finally for Cleanup: Always clean up resources (like database connections or file handles) in the finally block.

7. When Not to Use try...catch

While try...catch is a powerful tool for error handling, it’s not always the best approach in every scenario. Overusing try...catch or using it in inappropriate situations can lead to poor code quality, performance issues, or even masking important bugs. Here are some cases where you should avoid using try...catch:

1. Performance-Critical Code

Using try...catch in performance-sensitive code, such as inside loops or functions that are called frequently, can degrade performance. JavaScript engines optimize code better when there’s no try...catch block, so avoid using it in hot paths unless absolutely necessary.

// Avoid using try...catch inside frequently called functions
for (let i = 0; i < 1000000; i++) {
  try {
    // Code here
  } catch (e) {
    // Handle error
  }
}

In this case, it’s better to prevent errors through validation rather than handling them with try...catch.

2. Control Flow Shouldn’t Depend on Exceptions

Exceptions should be used for truly exceptional cases, not for normal control flow. Relying on try...catch to manage control flow makes the code harder to read and maintain. Instead, handle expected conditions with if-statements or other logic.

Avoid this:

try {
  const parsedData = JSON.parse(userInput);
  // Process parsed data
} catch (error) {
  console.log("Invalid JSON input");
}

Prefer this:

if (isValidJSON(userInput)) {
  const parsedData = JSON.parse(userInput);
  // Process parsed data
} else {
  console.log("Invalid JSON input");
}

3. Avoid try...catch for Synchronous Code with No Side Effects

If your code runs synchronously and can be easily debugged or validated before execution, it’s better to avoid using try...catch. For instance, errors caused by referencing undefined variables or calling non-existent functions should be fixed during development, not handled by try...catch.

// Avoid catching this error. Just fix the reference.
try {
  const result = undefinedVariable;
} catch (error) {
  console.error("Error: Variable is not defined");
}

Here, the error can be avoided by proper code practices and TypeScript’s type system, rather than relying on try...catch.

4. Validation Should Happen Beforehand

In many cases, input validation can prevent errors from ever being thrown. Instead of handling validation errors with try...catch, perform input validation ahead of time to avoid exceptions altogether.

Instead of this:

try {
  const result = divide(10, 0);
} catch (error) {
  console.error("Division by zero");
}

Do this:

function divide(a: number, b: number): number {
  if (b === 0) {
    console.error("Division by zero is not allowed");
    return NaN; // Or handle it differently
  }
  return a / b;
}

const result = divide(10, 0);

5. When Errors Should Bubble Up

Sometimes, it’s better to let errors propagate up the call stack rather than handling them immediately. If the current function can’t effectively handle the error, allowing it to bubble up may let a higher-level part of the code deal with it more appropriately, such as in a centralized error handler.

function fetchData() {
  // If an error occurs here, let it bubble up
  return fetch("/data");
}

try {
  const data = fetchData();
} catch (error) {
  // Handle all errors here
  console.error("Failed to fetch data");
}

This approach centralizes error handling, making it easier to manage, especially in larger applications.


8. Wrapping Up

Error handling in JavaScript and TypeScript is crucial for building resilient applications that can recover from unexpected issues. While JavaScript’s try…catch provides a straightforward mechanism to handle errors, TypeScript enhances this with static typing, reducing the risk of runtime errors.

However, it’s important to recognize when not to use try…catch, such as in performance-critical sections, for control flow, or when proper validation and error prevention techniques are more appropriate. By applying these best practices, you can ensure that your applications are robust, efficient, and maintainable.

Explore Other Resources

February 14, 2024 in Case Studies, Product Engineering

CLAS – A system that integrates Hubspot, Stripe, Canvas

About This Project Ziplines is a series A-funded ed-tech startup with one goal—helping students attain the real-world skills they need to thrive in careers they love by partnering with universities.…
Read More
September 9, 2024 in blog

Gentle Introduction to Elasticsearch

Elasticsearch is a search engine based on the Lucene library. It provides a distributed, multitenant-capable full-text search engine with an HTTP web interface and schema-free JSON documents. Elasticsearch is developed…
Read More
May 17, 2024 in blog

How to fix “OAuth out-of-band (OOB) flow will be deprecated” error for Google apps API access.

Migrate your OAuth out-of-band flow to an alternative method. Google has announced that they will block the usage of OOB based OAuth starting from January 31, 2023. This has forced…
Read More