No matter how experienced a developer you are, bugs are inevitable. Debugging is a core skill that involves identifying, understanding, and resolving problems in your code. Combined with effective error handling, it can drastically improve your development efficiency and the reliability of your applications.
In this eighth blog in the JavaScript Essentials series, we’ll explore how JavaScript handles errors, the most effective debugging techniques, and strategies for writing robust code that fails gracefully when something goes wrong.
1. Types of JavaScript Errors
Understanding error types is the first step to debugging effectively.
SyntaxError
Occurs when code violates JavaScript’s grammar rules.
let x = ; //
❌
SyntaxError
ReferenceError
Accessing a variable that hasn’t been declared.
console.log(a); //
❌
ReferenceError
TypeError
Calling a method on a non-function or accessing a property on undefined
or null
.
let name = null;
name.toUpperCase(); //
❌
TypeError
RangeError
When a value is not within a valid range (e.g., exceeding call stack).
function recurse() {
recurse();
}
recurse(); //
❌
RangeError: Maximum call stack size exceeded
URIError, EvalError, AggregateError – less common, but useful to know.
2. The try...catch
Block
Use try...catch
to handle errors gracefully and avoid crashes.
try {
let result = riskyFunction();
console.log(result);
} catch (error) {
console.error("An error occurred:", error.message);
}
Optional finally
block:
Always executes, whether an error occurred or not.
finally {
console.log("Cleaning up...");
}
3. Throwing Custom Errors
You can throw your own errors for validation or business logic.
function divide(a, b) {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
try {
divide(5, 0);
} catch (e) {
console.error(e.message);
}
You can also define custom error types:
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
4. Debugging with Browser Developer Tools
Most browsers (like Chrome, Firefox, Edge) provide powerful DevTools.
Opening DevTools
- Right-click > Inspect
- Press
F12
orCtrl+Shift+I
Console Tab
Use for:
- Logging
- Testing expressions
- Viewing error messages
Sources Tab
- View all loaded scripts
- Set breakpoints
- Step through code line by line
- Watch expressions and variables
5. Using Console Methods
Besides console.log()
, use other helpful methods:
console.error("Error message");
console.warn("Warning!");
console.info("Some info");
console.table([{ name: "Alice" }, { name: "Bob" }]);
console.assert(x > 10, "x is not greater than 10");
6. Understanding the Call Stack
The stack trace shows the path the program took before hitting the error.
function a() {
b();
}
function b() {
c();
}
function c() {
throw new Error("Something broke");
}
a();
Check the DevTools stack trace to locate the exact function call that caused the issue.
7. Debugging Techniques
Step-by-step Debugging
Use DevTools:
- Set breakpoints
- Step over (
F10
) - Step into (
F11
) - Step out (
Shift+F11
)
Using Watchers
Track variable values as you step through the code.
Conditional Breakpoints
Pause only if a condition is met.
// Right-click a line number > Add Conditional Breakpoint
8. Real Example: Debugging an API Call
async function fetchData() {
try {
const res = await fetch('/api/data');
if (!res.ok) throw new Error("Server error");
const data = await res.json();
console.log(data);
} catch (err) {
console.error("Failed to fetch:", err);
}
}
What to check:
- Is the endpoint correct?
- Is the network request succeeding?
- Is the data returned as expected?
9. Handling Async Errors
Async functions must wrap code in try...catch
.
async function getUser() {
try {
let res = await fetch("/user");
let user = await res.json();
return user;
} catch (e) {
console.error("Could not fetch user", e);
}
}
10. Using Linters and Formatters
Linters catch syntax errors and bad practices before runtime.
Popular tools:
- ESLint
- Prettier
These tools integrate with most code editors and CI pipelines.
11. Fallbacks and Defensive Programming
Use Optional Chaining
let city = user?.address?.city;
Set Defaults with Nullish Coalescing
let name = user.name ?? "Anonymous";
Sanitize Inputs
function setAge(age) {
if (typeof age !== "number") throw new TypeError("Age must be a number");
}
12. Logging Best Practices
- Log meaningful, structured messages
- Avoid
console.log()
in production (use logging libraries) - Use
console.group()
andconsole.time()
for performance monitoring
13. Testing Edge Cases
Manually test or write unit tests for:
- Empty inputs
- Unexpected values
- Slow or failed network responses
- Rapid user interactions (click spamming, form abuse)
14. Common Pitfalls to Avoid
❌ Swallowing errors:
try {
riskyFunction();
} catch {} // Bad: hides all info
✅ Always handle or log errors meaningfully:
catch (err) {
console.error("Unexpected error:", err);
}
❌ Blindly using try...catch
everywhere
✅ Use only where failure is likely
15. Summary Table
Technique | Use Case |
try...catch | Catching runtime errors |
DevTools Console | Testing and logging |
Breakpoints | Pausing and inspecting live code |
console.error , .warn | Clearer messaging |
Linters | Catch issues before they run |
Optional chaining (?. ) | Safely access deeply nested properties |
Nullish coalescing (?? ) | Provide fallback values |
Conclusion
Debugging and error handling are not just about fixing bugs—they’re about writing code that can fail safely and be maintained with confidence. Mastering these tools and techniques will make you a more efficient and reliable JavaScript developer.
In our next blog, we’ll explore building a Dynamic To-Do List App to apply many of the concepts we’ve covered—DOM manipulation, events, arrays, functions, and more.