Should You Wrap Every Function in a try-catch in JavaScript? Exploring the Good, the Bad, and the Ugly

Chirag Patel
5 min readNov 10, 2024

In JavaScript, try-catch is a powerful tool for handling errors gracefully. But should you apply it to every function? In this post, we’ll examine the pros and cons of try-catch, with real-world examples, alternative approaches, and insights from Clean Code by Robert C. Martin.

Why Use try-catch?

The try-catch block:

  1. Prevents runtime errors from propagating up and potentially crashing an application.
  2. Enables custom error handling and debugging.
  3. Allows graceful handling of errors, improving user experience.

Here’s a basic try-catch example:

try {
let data = fetchData(); // May throw an error
console.log(data);
} catch (error) {
console.error("Error fetching data:", error);
}

But does every function require this level of error handling? Let’s explore further.

Pros of Using try-catch Liberally

  1. Error Isolation and Prevention of Crashes
    Using try-catch helps prevent a single error from breaking an entire application.
  2. Improved User Experience
    Wrapping functions in try-catch can catch potential errors and enable fallback strategies, avoiding a poor user experience due to crashes.
  3. Enhanced Debugging in Large Codebases
    try-catch helps pinpoint failing functions in complex applications, making debugging easier.

Cons of Using try-catch Everywhere

  1. Masking Underlying Issues
    Excessive use of try-catch can hide underlying bugs, especially if errors are swallowed without proper logging.
  2. Code Complexity
    Wrapping every function in try-catch makes code harder to read and maintain, increasing cognitive load and development effort.
  3. Performance Degradation
    Overusing try-catch impacts performance, as JavaScript engines optimize code without it more easily.
function simpleMath(a, b) {
try {
return a + b;
} catch (error) {
console.error("Error in simpleMath:", error);
return null;
}
}

Here, try-catch is unnecessary for basic operations like addition, where exceptions are highly unlikely.

Example of When Not to Use try-catch:

Validation and Input Checks

Consider a function that validates a user’s input and throws an error for invalid data. Instead of wrapping each step in try-catch, you can handle errors at the top level by validating input before processing it:

function validateUserInput(input) {
if (typeof input !== "string" || input.trim() === "") {
throw new Error("Invalid input: must be a non-empty string.");
}
// Process valid input here
}

function processData(input) {
validateUserInput(input);
// Additional data processing here
}

try {
processData("example data");
} catch (error) {
console.error("Error processing data:", error);
}

In this example, the input is validated before processing, so if validateUserInput throws an error, it’s caught at the top level. This keeps individual functions focused, without excessive try-catch blocks at every level.

Another Example: String Manipulation

String operations like splitting or converting cases are generally safe, and adding try-catch for every such function would clutter the code:

function capitalizeWords(sentence) {
return sentence
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}

console.log(capitalizeWords("hello world")); // Outputs "Hello World"

Here, try-catch is unnecessary as the operations are predictable, and errors (e.g., the sentence being null) can be prevented with validation before calling capitalizeWords.

Example of Internal Functions with Known Behavior

In functions like tracking, where logic relies on stable internal functions, try-catch might not be necessary. Here’s an example:

export const tracking = ({
trackOmniture,
trackPDT,
tracking,
}
) => {
const { type, omnitureEventName, propName, pdtEventName } = trackingInfo;

if (tracking?.omnitureID) {
trackOmniture(type, omnitureEventName, propName, tracking.omnitureID, tracking.omnitureID);
}

if (tracking?.pdtTrackingId) {
trackPDT(type, pdtEventName, { action_name: tracking.pdtTrackingId });
}
};

Here, we avoid try-catch because logToOmniture and logToNewPDT are internal and handle their own potential errors. Using try-catch only when these functions are likely to throw errors, or if they interact with external APIs, keeps the code clean and focused.

When to Use try-catch Selectively

Selective use of try-catch is a practical approach in JavaScript. Here’s when to consider using try-catch and when it might be redundant:

High-Level Functions with Unpredictable Behavior

For functions that interact with external APIs, and databases, or perform asynchronous operations, try-catch is essential. Errors from such operations are more likely and harder to predict, as in this example:

async function fetchUserData(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error("Failed to fetch user data");
}
return await response.json();
} catch (error) {
console.error("Error fetching user data:", error);
return null; // or a fallback value
}
}

Error-Handling for Bulk Operations

When processing bulk data, selectively using try-catch can prevent the failure of the entire operation if only one part fails. Here’s an example:

function processBatch(dataArray) {
dataArray.forEach((data) => {
try {
processData(data); // Individual item processing
} catch (error) {
console.warn("Error processing data item:", data, error);
}
});
}
  • In this example, errors in processing individual items don’t impact the entire batch, enhancing resilience.

Top-Level Error Handling in Modules or Files

Place a try-catch at the highest level of a module to capture any unhandled exceptions within that module:

try {
mainFunction(); // Orchestrates various sub-functions
} catch (error)
{
console.error("Critical error in main module:", error);
}

Using try-catch in top-level functions captures any unexpected errors from dependencies or sub-functions while keeping code clean.

Principles from Clean Code: Keep Error Handling Clean and Focused

Robert C. Martin emphasizes simplicity in error handling in Clean Code. Here’s how you can apply these principles:

  1. Use try-catch at Higher Levels of Abstraction
    Place try-catch in high-level functions that orchestrate calls to lower-level functions. This isolates error handling from specific logic, keeping code clean.
  2. Handle Errors at the Source
    For functions with specialized behaviour, like logToOmniture, handle potential issues directly inside those functions, keeping other functions free of unnecessary error-handling code.
  3. Centralized Error Handling
    In web applications, use global error-handling functions, like the unhandledrejection event, to handle any uncaught exceptions without redundant try-catch in every function:
window.addEventListener("unhandledrejection", (event) => {
console.error("Unhandled promise rejection:", event.reason);
});

When to Use try-catch Selectively

In conclusion, applying try-catch to every function isn’t practical. Instead:

  • Reserve try-catch for unpredictable functions, those with dependencies, or critical operations.
  • Rely on validation or internal error handling for deterministic functions.
  • Follow Clean Code principles, using try-catch strategically at high levels and keeping error handling clear and manageable.

Further Reading and References

Sign up to discover human stories that deepen your understanding of the world.

Chirag Patel
Chirag Patel

Written by Chirag Patel

Software developer, Love Programming, Work in React, Redux, Node.

No responses yet

Write a response