Errors are inevitable. Good code doesn’t just work when everything goes right — it stays predictable and safe when things go wrong. In JavaScript, error handling is mainly done with three tools: try...catch blocks — to catch and handle exceptions throw — to create and raise your own errors when something is invalid Patterns to handle async errors, because JavaScript is heavily asynchronous try...catch blocks — to catch and handle exceptions try...catch throw — to create and raise your own errors when something is invalid throw Patterns to handle async errors, because JavaScript is heavily asynchronous This article goes beyond the basics. We’ll cover: Why JavaScript errors happen How try...catch really works under the hood The purpose of throw and when to use it How to handle errors in promises and async/await Real-world design patterns: input validation, fallback values, logging, and user feedback Why JavaScript errors happen How try...catch really works under the hood try...catch The purpose of throw and when to use it throw How to handle errors in promises and async/await Real-world design patterns: input validation, fallback values, logging, and user feedback 1. What is an error in JavaScript? When the JavaScript engine encounters a problem it cannot resolve — like trying to access an undefined variable, calling a function that does not exist, or failing to parse data — it throws an error. If this error is not handled, it bubbles up and can crash your script. // Uncaught ReferenceError console.log(user.name); // ReferenceError: user is not defined // Uncaught ReferenceError console.log(user.name); // ReferenceError: user is not defined The program stops here if you don’t catch this problem. 2. The Try-Catch Mechanism Purpose: try...catch lets you safely run code that might fail, and handle the failure in a controlled way. Purpose: try...catch How it works: How it works: Code inside the try block runs normally. If an error is thrown inside try, JavaScript stops running the rest of try immediately. Control jumps to the catch block. The catch block gets the error object with information about what went wrong. Code inside the try block runs normally. try If an error is thrown inside try, JavaScript stops running the rest of try immediately. try try Control jumps to the catch block. catch The catch block gets the error object with information about what went wrong. catch Basic syntax: Basic syntax: try { // code that might fail } catch (error) { // code to handle the failure } try { // code that might fail } catch (error) { // code to handle the failure } If no error occurs in try, the catch block is skipped entirely. try catch Example: Parsing JSON that might be malformed Example: try { const jsonString = '{"name":"Alice"}'; const user = JSON.parse(jsonString); console.log(user.name); // Alice // This JSON is invalid: missing quote const badJson = '{"name": Alice}'; JSON.parse(badJson); } catch (err) { console.error("JSON parsing failed:", err.message); } try { const jsonString = '{"name":"Alice"}'; const user = JSON.parse(jsonString); console.log(user.name); // Alice // This JSON is invalid: missing quote const badJson = '{"name": Alice}'; JSON.parse(badJson); } catch (err) { console.error("JSON parsing failed:", err.message); } Use try...catch only around risky operations: user input parsing, network requests, and file operations. try...catch 3. The throw Statement — Creating Your Own Errors throw Sometimes, your program detects a problem that the JavaScript engine itself would not consider an error. For example, maybe a number is negative when it should not be. To handle this, you can throw your own errors. Basic syntax: Basic syntax: throw new Error("Something went wrong"); throw new Error("Something went wrong"); When you throw: throw Execution immediately stops at the throw. Control looks for the nearest catch block up the call stack. If no catch exists, the program crashes. Execution immediately stops at the throw. Control looks for the nearest catch block up the call stack. catch If no catch exists, the program crashes. catch Example: Validating a function argument Example: function calculateArea(radius) { if (radius <= 0) { throw new Error("Radius must be positive"); } return Math.PI * radius * radius; } try { console.log(calculateArea(5)); // Works fine console.log(calculateArea(-2)); // Throws } catch (err) { console.error("Calculation failed:", err.message); } function calculateArea(radius) { if (radius <= 0) { throw new Error("Radius must be positive"); } return Math.PI * radius * radius; } try { console.log(calculateArea(5)); // Works fine console.log(calculateArea(-2)); // Throws } catch (err) { console.error("Calculation failed:", err.message); } Use throw when you hit a state that should never happen in correct usage. It enforces contracts: "This function must not get bad input." throw 4. The finally Block — Guaranteed Cleanup finally finally always runs, whether the code in try succeeds, fails, or even if you return from try. finally try return try Example: try { console.log("Opening connection"); throw new Error("Connection failed"); } catch (err) { console.error("Error:", err.message); } finally { console.log("Closing connection"); } // Output: // Opening connection // Error: Connection failed // Closing connection try { console.log("Opening connection"); throw new Error("Connection failed"); } catch (err) { console.error("Error:", err.message); } finally { console.log("Closing connection"); } // Output: // Opening connection // Error: Connection failed // Closing connection Use finally for closing files or database connections, stopping loaders/spinners, or resetting states. finally 5. Asynchronous Code: The Common Trap JavaScript runs lots of code asynchronously — setTimeout, fetch, promises. try...catch does not automatically catch errors that happen inside callbacks or promises. try...catch Example: try { setTimeout(() => { throw new Error("Oops"); }, 1000); } catch (err) { console.log("Caught:", err.message); // Never runs } try { setTimeout(() => { throw new Error("Oops"); }, 1000); } catch (err) { console.log("Caught:", err.message); // Never runs } How to Handle Async Errors Properly Wrap inside the async function Wrap inside the async function setTimeout(() => { try { throw new Error("Oops inside timeout"); } catch (err) { console.error("Caught:", err.message); } }, 1000); setTimeout(() => { try { throw new Error("Oops inside timeout"); } catch (err) { console.error("Caught:", err.message); } }, 1000); Promises: use .catch Promises: use .catch fetch("https://bad.url") .then(res => res.json()) .catch(err => console.error("Network or parsing failed:", err.message)); fetch("https://bad.url") .then(res => res.json()) .catch(err => console.error("Network or parsing failed:", err.message)); Async/await: wrap with try-catch Async/await: wrap with try-catch async function fetchUser() { try { const response = await fetch("https://bad.url"); const data = await response.json(); console.log(data); } catch (err) { console.error("Async/await failed:", err.message); } } fetchUser(); async function fetchUser() { try { const response = await fetch("https://bad.url"); const data = await response.json(); console.log(data); } catch (err) { console.error("Async/await failed:", err.message); } } fetchUser(); 6. Real-World Example: Form Validation Putting it together with a user registration check. function registerUser(user) { if (!user.username) { throw new Error("Username is required"); } if (user.password.length < 8) { throw new Error("Password must be at least 8 characters"); } return "User registered!"; } try { const user = { username: "John", password: "123" }; const result = registerUser(user); console.log(result); } catch (err) { console.error("Registration failed:", err.message); } function registerUser(user) { if (!user.username) { throw new Error("Username is required"); } if (user.password.length < 8) { throw new Error("Password must be at least 8 characters"); } return "User registered!"; } try { const user = { username: "John", password: "123" }; const result = registerUser(user); console.log(result); } catch (err) { console.error("Registration failed:", err.message); } 7. Logging and Rethrowing Catch an error just to log it, then rethrow so a higher-level handler can deal with it. function processData(data) { try { if (!data) { throw new Error("No data"); } // process... } catch (err) { console.error("Log:", err.message); throw err; // propagate further } } try { processData(null); } catch (err) { console.log("Final catch:", err.message); } function processData(data) { try { if (!data) { throw new Error("No data"); } // process... } catch (err) { console.error("Log:", err.message); throw err; // propagate further } } try { processData(null); } catch (err) { console.log("Final catch:", err.message); } 8. Best Practices Never ignore errors silently. Add meaningful, precise messages. Use custom error classes for clarity if needed. Catch only what you can handle. Don’t catch and swallow everything. For async operations, always .catch() promises or use try-catch with await. Always log unexpected errors somewhere. Never ignore errors silently. Add meaningful, precise messages. Use custom error classes for clarity if needed. Catch only what you can handle. Don’t catch and swallow everything. For async operations, always .catch() promises or use try-catch with await. .catch() try-catch await Always log unexpected errors somewhere. Conclusion Bad things happen in software — it’s how you prepare for them that separates a robust application from a fragile one. Handle predictable failures, fail loudly on developer errors, and never let unexpected problems silently break your users’ trust. If you understand how try...catch, throw, and async error handling fit together, you have a safety net for whatever the real world throws at your code. try...catch throw