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

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 oftry-catch
, with real-world examples, alternative approaches, and insights from Clean Code by Robert C. Martin.
Why Use try-catch
?
The try-catch
block:
- Prevents runtime errors from propagating up and potentially crashing an application.
- Enables custom error handling and debugging.
- 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
- Error Isolation and Prevention of Crashes
Usingtry-catch
helps prevent a single error from breaking an entire application. - Improved User Experience
Wrapping functions intry-catch
can catch potential errors and enable fallback strategies, avoiding a poor user experience due to crashes. - Enhanced Debugging in Large Codebases
try-catch
helps pinpoint failing functions in complex applications, making debugging easier.
Cons of Using try-catch
Everywhere
- Masking Underlying Issues
Excessive use oftry-catch
can hide underlying bugs, especially if errors are swallowed without proper logging. - Code Complexity
Wrapping every function intry-catch
makes code harder to read and maintain, increasing cognitive load and development effort. - Performance Degradation
Overusingtry-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:
- Use
try-catch
at Higher Levels of Abstraction
Placetry-catch
in high-level functions that orchestrate calls to lower-level functions. This isolates error handling from specific logic, keeping code clean. - Handle Errors at the Source
For functions with specialized behaviour, likelogToOmniture
, handle potential issues directly inside those functions, keeping other functions free of unnecessary error-handling code. - Centralized Error Handling
In web applications, use global error-handling functions, like theunhandledrejection
event, to handle any uncaught exceptions without redundanttry-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
- JavaScript: Understanding the Weird Parts by Tony Alicea — A great course for understanding JavaScript fundamentals and error handling.
- Clean Code: A Handbook of Agile Software Craftsmanship by Robert C. Martin — Essential reading for writing clean, maintainable code, including best practices for error handling.
- Mozilla Developer Network (MDN) on Error Handling in JavaScript — Reference documentation and examples for using