Welcome to Error Diagnosis 101
Every developer encounters errors - it's like being a detective where the clues are error messages and the mystery is finding the bug. Today, we'll explore the most common JavaScript errors, understand why they happen, and learn how to fix them. By the end of this lesson, you'll be able to diagnose and resolve errors like a seasoned developer!
The Error Hierarchy
1. SyntaxError: Unexpected Token
This error occurs when JavaScript encounters code it doesn't understand.
Common Example:
// ERROR: Missing closing bracket
function calculateTotal(items) {
let total = 0;
for (let item of items) {
total += item.price;
// Missing closing bracket!
return total;
}
// ERROR: Unexpected token
const user = {
name: "John",
age: 30,
address: "123 Main St", // Extra comma
};
// ERROR: Invalid JSON
const data = JSON.parse('{"name": "John", "age": 30,}'); // Trailing comma
Solution:
// CORRECT: All brackets properly closed
function calculateTotal(items) {
let total = 0;
for (let item of items) {
total += item.price;
}
return total;
}
// CORRECT: No trailing comma
const user = {
name: "John",
age: 30,
address: "123 Main St"
};
// CORRECT: Valid JSON
const data = JSON.parse('{"name": "John", "age": 30}');
// TIP: Use JSON.stringify for safe JSON creation
const safeJson = JSON.stringify({ name: "John", age: 30 });
2. ReferenceError: Variable Not Defined
This occurs when you try to use a variable that hasn't been declared.
Common Examples:
// ERROR: Variable used before declaration
console.log(username);
let username = "Alice";
// ERROR: Misspelled variable name
let userCount = 10;
console.log(usercount); // lowercase 'c'
// ERROR: Variable out of scope
function doSomething() {
let localVar = "I'm local";
}
console.log(localVar); // Can't access function variable
// ERROR: Forgot to declare variable
for (i = 0; i < 5; i++) { // 'i' not declared
console.log(i);
}
Solutions:
// CORRECT: Declare before use
let username = "Alice";
console.log(username);
// CORRECT: Use correct variable name
let userCount = 10;
console.log(userCount); // Correct case
// CORRECT: Return value from function
function doSomething() {
let localVar = "I'm local";
return localVar;
}
console.log(doSomething());
// CORRECT: Always declare variables
for (let i = 0; i < 5; i++) {
console.log(i);
}
// TIP: Use 'use strict' to catch undeclared variables
'use strict';
// Now this will throw an error:
// myVar = 10; // Error: myVar is not defined
3. TypeError: Cannot Read Property of Undefined/Null
This happens when you try to access a property of undefined or null.
Common Examples:
// ERROR: Accessing property of undefined
let user;
console.log(user.name);
// ERROR: Chaining properties that might not exist
let data = { user: { /* name might be missing */ } };
console.log(data.user.name.length);
// ERROR: Array method on undefined
let items;
items.forEach(item => console.log(item));
// ERROR: Calling method on null
let element = document.getElementById('nonexistent');
element.addEventListener('click', handler);
Solutions:
// SOLUTION 1: Check for existence
let user;
if (user) {
console.log(user.name);
}
// SOLUTION 2: Optional chaining (?.)
let data = { user: {} };
console.log(data.user?.name?.length);
// SOLUTION 3: Provide default values
let items = [];
items.forEach(item => console.log(item));
// SOLUTION 4: Null checking
let element = document.getElementById('nonexistent');
if (element) {
element.addEventListener('click', handler);
}
// SOLUTION 5: Nullish coalescing (??)
const username = user?.name ?? 'Guest';
// SOLUTION 6: Defensive programming
function getProperty(obj, path) {
return path.split('.').reduce((acc, part) => acc && acc[part], obj);
}
console.log(getProperty(data, 'user.name.length')); // Safe access
4. TypeError: X is Not a Function
This occurs when you try to call something that isn't a function.
Common Examples:
// ERROR: Calling a non-function value
let result = "hello";
result(); // Strings aren't functions
// ERROR: Overwriting a function with a value
let calculate = (a, b) => a + b;
calculate = calculate(5, 3); // Now calculate is 8, not a function
calculate(2, 2); // Error!
// ERROR: Incorrect method name
let numbers = [1, 2, 3];
numbers.maps(n => n * 2); // Should be 'map'
// ERROR: Forgetting parentheses in method call
let user = {
getName: function() {
return this.name;
}
};
console.log(user.getName); // Returns function, doesn't call it
Solutions:
// CORRECT: Only call functions
let result = () => "hello";
result(); // This works
// CORRECT: Don't overwrite functions
let calculate = (a, b) => a + b;
let result = calculate(5, 3); // Store result separately
console.log(calculate(2, 2)); // Function still works
// CORRECT: Use correct method names
let numbers = [1, 2, 3];
numbers.map(n => n * 2); // Correct method name
// CORRECT: Call the function
let user = {
getName: function() {
return this.name;
}
};
console.log(user.getName()); // Note the parentheses
// TIP: Check if something is a function before calling
if (typeof myVar === 'function') {
myVar();
}
5. RangeError: Maximum Call Stack Size Exceeded
This happens when you have infinite recursion or very deep function calls.
Common Examples:
// ERROR: Infinite recursion
function countDown(n) {
console.log(n);
countDown(n - 1); // No base case!
}
countDown(10);
// ERROR: Circular references in recursion
function processNode(node) {
processNode(node.parent); // What if parent references child?
}
// ERROR: Event handler triggering itself
element.addEventListener('click', function() {
element.click(); // Triggers the same handler!
});
// ERROR: Deep recursion without tail call optimization
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
fibonacci(1000); // Too deep!
Solutions:
// SOLUTION 1: Add base case
function countDown(n) {
if (n <= 0) return; // Base case
console.log(n);
countDown(n - 1);
}
// SOLUTION 2: Check for circular references
function processNode(node, visited = new Set()) {
if (!node || visited.has(node)) return;
visited.add(node);
processNode(node.parent, visited);
}
// SOLUTION 3: Prevent recursive event triggering
let isProcessing = false;
element.addEventListener('click', function() {
if (isProcessing) return;
isProcessing = true;
// Do work
isProcessing = false;
});
// SOLUTION 4: Use iteration instead of recursion
function fibonacciIterative(n) {
if (n <= 1) return n;
let a = 0, b = 1;
for (let i = 2; i <= n; i++) {
[a, b] = [b, a + b];
}
return b;
}
// SOLUTION 5: Memoization for recursive functions
function fibonacciMemo(n, memo = {}) {
if (n in memo) return memo[n];
if (n <= 1) return n;
memo[n] = fibonacciMemo(n - 1, memo) + fibonacciMemo(n - 2, memo);
return memo[n];
}
6. TypeError: Assignment to Constant Variable
This occurs when you try to reassign a const variable.
Common Examples:
// ERROR: Reassigning const
const PI = 3.14159;
PI = 3.14; // Error!
// ERROR: Reassigning const in loop
const scores = [85, 92, 78];
for (const score of scores) {
score = score + 5; // Error!
}
// ERROR: Const with undefined initial value
const username;
username = "Alice"; // Error!
Solutions:
// SOLUTION 1: Use let for variables that change
let PI = 3.14159;
PI = 3.14; // This works
// SOLUTION 2: Use let in loops when modifying values
const scores = [85, 92, 78];
let adjustedScores = [];
for (let score of scores) {
adjustedScores.push(score + 5);
}
// SOLUTION 3: Initialize const variables immediately
const username = "Alice";
// SOLUTION 4: Const objects can have properties modified
const user = { name: "Alice" };
user.name = "Bob"; // This is allowed
user.age = 30; // This is allowed
// SOLUTION 5: Use Object.freeze for immutable objects
const frozenUser = Object.freeze({ name: "Alice" });
// frozenUser.name = "Bob"; // This will fail silently (or error in strict mode)
7. Async/Await Errors
Common mistakes when working with asynchronous code.
Common Examples:
// ERROR: Forgot to use await
async function fetchData() {
const response = fetch('/api/data'); // Missing await
const data = response.json(); // This will fail
return data;
}
// ERROR: Using await outside async function
function regularFunction() {
const data = await fetchData(); // Error!
}
// ERROR: Not handling promise rejections
async function riskyOperation() {
const data = await fetchData(); // What if this fails?
console.log(data);
}
// ERROR: Parallel execution turned sequential
async function fetchMultiple() {
const user = await fetchUser();
const posts = await fetchPosts();
const comments = await fetchComments();
// These run sequentially, not in parallel
}
Solutions:
// SOLUTION 1: Always use await with promises
async function fetchData() {
const response = await fetch('/api/data');
const data = await response.json();
return data;
}
// SOLUTION 2: Make function async to use await
async function correctFunction() {
const data = await fetchData();
return data;
}
// SOLUTION 3: Handle errors with try/catch
async function safeOperation() {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
console.error('Operation failed:', error);
}
}
// SOLUTION 4: Run promises in parallel
async function fetchMultipleParallel() {
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
]);
return { user, posts, comments };
}
// SOLUTION 5: Global error handler for unhandled rejections
process.on('unhandledRejection', (error) => {
console.error('Unhandled promise rejection:', error);
});
8. Common Logical Errors
These don't throw errors but cause incorrect behavior.
Common Examples:
// ERROR: Comparing with assignment
if (x = 5) { // Always true, assigns 5 to x
console.log("This always runs");
}
// ERROR: String concatenation instead of addition
console.log("2" + 2); // "22", not 4
// ERROR: Floating point precision
console.log(0.1 + 0.2 === 0.3); // false!
// ERROR: Array/Object comparison
console.log([1, 2] === [1, 2]); // false
console.log({a: 1} === {a: 1}); // false
// ERROR: Incorrect this binding
const obj = {
value: 42,
getValue: function() {
return this.value;
}
};
const getValue = obj.getValue;
console.log(getValue()); // undefined
Solutions:
// SOLUTION 1: Use comparison operator
if (x === 5) {
console.log("x equals 5");
}
// SOLUTION 2: Convert strings to numbers
console.log(Number("2") + 2); // 4
console.log(parseInt("2") + 2); // 4
// SOLUTION 3: Handle floating point precision
function areFloatsEqual(a, b, epsilon = 0.00001) {
return Math.abs(a - b) < epsilon;
}
console.log(areFloatsEqual(0.1 + 0.2, 0.3)); // true
// SOLUTION 4: Compare array/object contents
function arraysEqual(a, b) {
return a.length === b.length &&
a.every((val, index) => val === b[index]);
}
function objectsEqual(a, b) {
return JSON.stringify(a) === JSON.stringify(b);
}
// SOLUTION 5: Bind this correctly
const getValue = obj.getValue.bind(obj);
console.log(getValue()); // 42
// Or use arrow function in object
const obj2 = {
value: 42,
getValue: () => {
return this.value; // Note: this doesn't work as expected in arrow functions
}
};
// Better approach
class MyClass {
constructor() {
this.value = 42;
}
getValue = () => {
return this.value; // Arrow function preserves this
}
}
Error Prevention Strategies
1. Use Strict Mode
'use strict';
// Prevents accidental globals
mistypedVariable = 17; // Error in strict mode
// Prevents duplicate parameters
function sum(a, a, c) { // Error in strict mode
return a + a + c;
}
2. Type Checking
function safeOperation(value) {
// Check types before operations
if (typeof value !== 'number') {
throw new TypeError('Expected a number');
}
return value * 2;
}
// Use TypeScript or JSDoc for better type safety
/**
* @param {number} a
* @param {number} b
* @returns {number}
*/
function add(a, b) {
return a + b;
}
3. Defensive Programming
function processData(data) {
// Validate input
if (!data || typeof data !== 'object') {
console.error('Invalid data provided');
return null;
}
// Safe property access
const name = data.user?.name ?? 'Unknown';
const age = data.user?.age ?? 0;
// Handle edge cases
if (age < 0 || age > 150) {
console.warn('Suspicious age value:', age);
}
return { name, age };
}
Debugging Strategies
Systematic Debugging Process:
- Read the error message carefully
- Identify the file and line number
- Understand what the code is trying to do
- Check variable values at the error point
- Reproduce the error consistently
- Form a hypothesis about the cause
- Test your hypothesis
- Implement and verify the fix
Common Anti-Patterns to Avoid
// ANTI-PATTERN 1: Swallowing errors
try {
riskyOperation();
} catch (e) {
// Don't do this!
}
// BETTER: Always handle or log errors
try {
riskyOperation();
} catch (error) {
console.error('Operation failed:', error);
// Handle the error appropriately
}
// ANTI-PATTERN 2: Using exceptions for flow control
function findUser(id) {
throw new Error('User not found'); // Don't use exceptions for expected cases
}
// BETTER: Return null or use a result object
function findUser(id) {
return users.find(u => u.id === id) || null;
}
// ANTI-PATTERN 3: Overly broad error catching
try {
// Lots of different operations
operation1();
operation2();
operation3();
} catch (e) {
console.log('Something went wrong');
}
// BETTER: Specific error handling
try {
operation1();
} catch (e) {
handleOperation1Error(e);
}
try {
operation2();
} catch (e) {
handleOperation2Error(e);
}
Practice Exercises
Exercise 1: Debug This Code
// This code has multiple errors. Find and fix them:
function calculateAverage(numbers) {
let sum;
for (let i = 0; i <= numbers.length; i++) {
sum += numbers[i];
}
return sum / numbers.length;
}
const scores = [85, 92, 78, 90];
console.log("Average:", calculateAverage(scores));
Exercise 2: Error Handling Challenge
// Add proper error handling to this function:
async function fetchUserProfile(userId) {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
const posts = await fetch(`/api/users/${userId}/posts`);
const userPosts = await posts.json();
return {
user: user,
posts: userPosts,
totalLikes: userPosts.reduce((sum, post) => sum + post.likes, 0)
};
}
Key Takeaways
- Always read error messages carefully - they contain valuable information
- Understand the different types of errors: Syntax, Runtime, and Logical
- Use proper error handling with try/catch blocks
- Implement defensive programming practices
- Test edge cases and handle them appropriately
- Use debugging tools effectively
- Learn from errors - they make you a better developer