JAVASCRIPT • DEBUGGING AND TESTING

Common JavaScript Bugs — Recognize common bugs (undefined access, coercion issues, async timing) (conceptual)

Learn to spot and fix the three sneakiest categories of bugs that trip up JavaScript developers every day.

Why JavaScript Bugs Are So Common

JavaScript was famously created in just ten days in May 1995 by Brendan Eich at Netscape. Because the language was designed under extreme time pressure, several quirky behaviors were baked in from the start — behaviors that still cause bugs today. Unlike stricter languages such as Java or C++, JavaScript is dynamically typed, meaning variables do not need a declared type and can change type at any time. This flexibility is powerful, but it also opens the door to subtle errors that a compiler would catch in other languages.

1995
JavaScript Is Born
Brendan Eich creates JavaScript in 10 days for Netscape Navigator. Its loose typing and automatic coercion are built in from the start.
2005
AJAX and Async Patterns Emerge
Web apps start making background network requests. Callback-based async code becomes common, introducing a new class of timing bugs.
2009
ES5 Introduces Strict Mode
ECMAScript 5 adds "use strict" to catch silent errors. It prevents some, but not all, of the language's common pitfalls.
2015
ES6 Brings Promises and let/const
Modern JavaScript introduces Promises for cleaner async code and block-scoped variables. Many classic bugs become avoidable — if you know the patterns.
2020s
TypeScript Gains Popularity
TypeScript adds static types on top of JavaScript, catching undefined-access and coercion bugs at compile time. But understanding the underlying issues remains essential.

Even with modern tools, JavaScript's original design decisions still affect every developer who writes it. The three bug categories you will explore — undefined access, type coercion, and async timing — account for a huge proportion of real-world JavaScript errors. Understanding why they happen puts you far ahead of most beginners.

The Three Core Bug Categories

Most JavaScript bugs that beginners encounter fall into three broad families. Each one stems from a fundamental feature of the language itself: dynamic typing allows values to be undefined without warning, implicit conversion (coercion) silently transforms data types, and the event loop can finish asynchronous operations in a different order than you expect. Let's define each one clearly.

1

Undefined Access

Trying to read a property or variable that does not exist or has not been assigned yet. JavaScript returns undefined instead of throwing an error — and calling a method on undefined crashes your program with a TypeError.
2

Type Coercion Issues

JavaScript automatically converts values between types (string, number, boolean) during comparisons and operations. This implicit coercion can produce surprising results like "5" + 3 returning "53" instead of 8.
3

Async Timing Bugs

JavaScript is single-threaded but handles asynchronous tasks (network requests, timers) by scheduling them for later. Code that depends on an async result may run before that result arrives, leading to undefined values or wrong outputs.
✦ KEY TAKEAWAY
Think of JavaScript like a very eager assistant. If you ask for something that doesn't exist, it won't tell you — it just says "undefined" and moves on. If you mix up text and numbers, it guesses what you meant rather than asking for clarification. And if you send it to fetch something, it doesn't wait by the door — it keeps doing other tasks, so you might look for the delivery before it arrives. Each of these helpful behaviors becomes a bug when you're not expecting it.

How These Bugs Flow Through Your Code

The diagram below maps the three bug categories to their root causes and the symptoms you will see. Trace each path from left to right: it starts with a common coding pattern, passes through JavaScript's internal behavior, and ends with the visible bug. Recognizing these paths helps you diagnose problems quickly.

Each row traces a common bug from the code pattern that causes it, through JavaScript's internal behavior, to the visible symptom. Notice how coercion bugs produce silent wrong results (no crash), making them especially hard to detect.

The key insight from this diagram is that two of the three bug types — undefined access and coercion — can fail silently. JavaScript does not always throw an error when something goes wrong. Instead, it returns undefined or a coerced value and keeps running. This means your code may continue executing with bad data, and the actual symptom may appear far away from the original mistake. Developing the habit of checking your assumptions — "Is this value what I think it is?" — is the single most important debugging skill in JavaScript.

How Each Bug Category Works Under the Hood

Undefined Access: The Silent Failure

In JavaScript, when you access a property that does not exist on an object, the language returns the special value undefined without any warning. For example, if you write let user = {}; console.log(user.name); the console prints undefined because name was never set. This becomes a crash only when you try to access a property on that undefined value, like user.address.street. Since user.address is undefined, JavaScript cannot read .street from it, and throws a TypeError.

Type Coercion: JavaScript's Guessing Game

When you use an operator like + between different types, JavaScript follows a set of internal rules to convert one type to match the other. The + operator is especially tricky because it serves double duty: it adds numbers and concatenates strings. If either operand is a string, JavaScript converts the other to a string. So "5" + 3 becomes "53" (string concatenation), while "5" - 3 becomes 2 (numeric subtraction) because the - operator only works on numbers. The loose equality operator == also performs coercion before comparing, which is why 0 == "" returns true. Using the strict equality operator === avoids this because it checks type first.

Async Timing: The Event Loop Surprise

JavaScript runs on a single thread, meaning it can only do one thing at a time. When you start an asynchronous operation — like fetching data from a server with fetch() or setting a timer with setTimeout() — JavaScript does not pause and wait. Instead, it registers a callback function to run later and immediately moves to the next line of code. The event loop is the mechanism that checks whether the async task has completed and then runs the callback. The classic bug occurs when you write code that uses the result of an async operation on the very next line — that line runs before the async result is available, so you get undefined or stale data.

⚡ Why === Matters
A quick rule of thumb: always use === (strict equality) instead of == (loose equality) in your comparisons. Strict equality does not perform type coercion, so 0 === "" is false (as you would expect), while 0 == "" is true (which is almost never what you want).

Recognizing Bug Patterns in Real Code

Knowing the three categories is only half the battle. The real skill is recognizing these bugs when you see them in actual code. The table below lists the most common patterns, the output they produce, and the fix. Study these — you will encounter every single one of them as you write more JavaScript.

Common JavaScript bug patterns and their fixes
Buggy CodeCategoryOutput / ErrorFix
let x; console.log(x.length);Undefined AccessTypeError: Cannot read properties of undefinedCheck with if (x !== undefined) or use optional chaining x?.length
"5" + 3Coercion"53" (string, not 8)Convert explicitly: Number("5") + 3
"5" == 5Coerciontrue (unexpected)Use strict equality: "5" === 5 → false
let data; fetch(url).then(r => data = r); console.log(data);Async Timingundefined (fetch not finished)Use await or move logic inside .then()
for(var i=0;i<3;i++){setTimeout(()=>console.log(i),100);}Async Timing + ScopingLogs 3, 3, 3 (not 0, 1, 2)Replace var with let (block scoping)
The + operator checks if either value is a string. If yes, it concatenates; if no, it adds numerically. This dual behavior is the root cause of most coercion bugs involving the + operator.

Debugging a Realistic Code Snippet

Let's walk through a buggy function that contains all three categories of bugs. Imagine you are building a simple app that fetches a user's profile from an API and displays their name and age.

Finding Three Bugs in One Function
1
Step 1 — Read the Buggy CodeHere is the original code: let user; fetch('/api/user').then(res => res.json()).then(data => user = data); console.log('Name: ' + user.name); console.log('Age next year: ' + user.age + 1); Before running it, let's predict what will happen by analyzing each line.
2
Step 2 — Spot Bug #1: Async TimingThe fetch() call is asynchronous, so JavaScript does not wait for it to finish. The two console.log lines run immediately, but user is still undefined at that point because the fetch has not completed yet.
Bug #1: Async timing — code runs before the data arrives.
3
Step 3 — Spot Bug #2: Undefined AccessBecause user is undefined, accessing user.name throws a TypeError: Cannot read properties of undefined (reading 'name'). The program crashes here and never reaches the second log.
Bug #2: Undefined access — reading .name on an undefined variable.
4
Step 4 — Spot Bug #3: Coercion (Hidden)Even if the timing bug were fixed, the second log has a coercion bug. The expression 'Age next year: ' + user.age + 1 evaluates left to right. First, 'Age next year: ' + user.age produces a string (e.g., "Age next year: 16"). Then + 1 concatenates, giving "Age next year: 161" instead of "Age next year: 17".
Bug #3: Coercion — string + number concatenates instead of adding.
5
Step 5 — Write the Fixed VersionThe fixed code uses async/await to wait for the fetch, optional chaining for safety, and parentheses to force arithmetic before string concatenation: async function showUser() { const res = await fetch('/api/user'); const user = await res.json(); console.log('Name: ' + (user?.name ?? 'Unknown')); console.log('Age next year: ' + (user.age + 1)); } Now await pauses until the data arrives, ?. prevents undefined-access crashes, and (user.age + 1) ensures numeric addition happens first.
All three bugs fixed with await, optional chaining, and parentheses.

Prevention Strategies Compared

Now that you know the three bug categories, the natural question is: how do you prevent them? Different strategies work better for different bug types. The table below compares the most practical prevention methods you can start using today.

Prevention strategies vs. bug categories
StrategyPrevents Undefined Access?Prevents Coercion Bugs?Prevents Async Timing Bugs?
Use strict equality (===)Partially — catches null == undefined✓ Yes — no type coercion in comparisons✗ No
Optional chaining (?.)✓ Yes — returns undefined instead of crashing✗ No✗ No
Explicit type conversion✗ No✓ Yes — Number(), String(), Boolean()✗ No
async / await✗ No✗ No✓ Yes — pauses until async result arrives
Use let/const (not var)Partially — avoids hoisting confusion✗ No✓ Partially — fixes loop + setTimeout bug
TypeScript (static types)✓ Yes — catches at compile time✓ Yes — enforces typesPartially — types help, but timing is a logic issue
🛡️ KEY TAKEAWAY
There is no single silver bullet that prevents all three bug types. Think of debugging strategies like layers of armor: === protects against coercion, ?. protects against undefined crashes, and await protects against timing issues. The best developers combine all of these habits into their daily coding practice, so the bugs never appear in the first place.

Connecting to Advanced Debugging Tools

The techniques you have learned in this lesson are the foundation, but professional developers use additional tools to catch these bugs before they reach users. As you advance, you will encounter linters (like ESLint), type systems (like TypeScript), and testing frameworks (like Jest). Understanding the three bug categories makes these tools much easier to learn because you already know what they are trying to prevent.

From manual habits to professional tools
This Lesson (Manual Habits)Advanced ToolHow It Helps
Check for undefined before accessingTypeScriptCompiler refuses to build if a value could be undefined and you don't handle it
Use === instead of ==ESLint (eqeqeq rule)Warns you automatically whenever you write == instead of ===
Use await for async operationsESLint (no-floating-promises)Flags Promises that are not awaited or caught, preventing unhandled timing bugs
Manually test code outputsJest / VitestAutomated tests run hundreds of checks in seconds, catching bugs every time you change your code

The key takeaway for your learning journey is this: every advanced tool exists because of the bugs you studied today. When you eventually learn TypeScript, you will think, "Oh, this prevents undefined access bugs." When you configure ESLint, you will think, "This catches coercion issues for me." That understanding gives you a huge advantage over developers who learn the tools without understanding the why behind them.

Practice Problems

PROBLEM 1 — CONCEPTUAL
Explain in your own words why JavaScript returns undefined when you access a property that does not exist on an object, instead of immediately throwing an error. What is one advantage and one disadvantage of this behavior?
PROBLEM 2 — BASIC CALCULATION
Predict the output of each expression and name the bug category if one applies: (a) "10" - 5 (b) "10" + 5 (c) true + 1 (d) null == undefined
PROBLEM 3 — INTERMEDIATE
Consider this code: const config = { theme: { colors: null } }; const bg = config.theme.colors.background; (a) What error will this produce and why? (b) Rewrite the line that assigns bg using optional chaining and a fallback default value of "white".
PROBLEM 4 — APPLIED
You are building a weather app. The following function is supposed to display the temperature, but it always shows "Loading..." even after the data arrives. Identify the bug and write a corrected version. function showWeather() { let temp = 'Loading...'; fetch('/api/weather').then(res => res.json()).then(data => { temp = data.temperature; }); document.getElementById('display').textContent = temp; }
PROBLEM 5 — CRITICAL THINKING
A classmate argues: "I just use == everywhere because it's more flexible — it handles type differences for me automatically. Using === is extra work for no benefit." Write a thoughtful response that includes at least two concrete examples where == produces a surprising or dangerous result that === would have prevented. Then explain one rare case where == might actually be intentionally useful.

Lesson Summary

JavaScript's three most common bug categories are undefined access (reading properties on values that don't exist, leading to silent undefined returns or TypeError crashes), type coercion (JavaScript silently converting types during operations and comparisons, producing surprising results like "53" instead of 8), and async timing (code running before asynchronous results arrive because the event loop defers callbacks to execute later).

You can prevent these bugs by combining multiple strategies: use optional chaining (?.) and null checks for undefined access, strict equality (===) and explicit conversion functions like Number() for coercion bugs, and async/await for timing issues. As you grow as a developer, tools like TypeScript, ESLint, and testing frameworks will automate much of this detection — but understanding the root causes is what makes those tools effective.

Varsity Tutors • JavaScript • Common JavaScript Bugs — Recognize common bugs (undefined access, coercion issues, async timing) (conceptual)