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.

JavaScript emblem with code brackets, braces, and a code file.
Inside this lesson 10 sections 5 questions 5 figures

SECTION 1

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.

SECTION 2

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.

SECTION 3

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.

Common JavaScript Bug PathwaysCODE PATTERNJS BEHAVIORSYMPTOMobj.prop.namenested property accessprop is undefinedsilent undefined returnTypeError:Cannot read prop"5" + 3mixed-type operationimplicit coercionnumber → string silently"53"wrong result, no errorfetch(url)async operation startsevent loop deferscallback runs laterdata is undefinedused before it arrivesUndefined AccessType CoercionAsync TimingCrash / ErrorSilent Wrong Result
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.

SECTION 4

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).

SECTION 5

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.

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)

Common JavaScript bug patterns and their fixes

The + Operator Decision Treevalue1 + value2Is either operand a string?YESNOString Concatenation"5" + 3 → "53"Numeric Addition5 + 3 → 8Other operand is coerced to stringString(3) → "3", then "5" + "3" → "53"Both operands treated as numbers5 + 3 → 8TIP: Use Number() to convert strings before adding, or use parseInt()
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.

SECTION 6

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. 1

    Step 1 — Read the Buggy Code

    Here 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. 2

    Step 2 — Spot Bug #1: Async Timing

    The 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. 3

    Step 3 — Spot Bug #2: Undefined Access

    Because 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. 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. 5

    Step 5 — Write the Fixed Version

    The 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.

SECTION 7

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.

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

Prevention strategies vs. bug categories

🛡️

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.

SECTION 8

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.

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

From manual habits to professional tools

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.

SECTION 9

Practice Problems

Question 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?
Show answer
JavaScript was designed to be forgiving and flexible, especially for web pages where a crash would be a bad user experience. Returning undefined allows code to keep running even if a property is missing, which is an advantage when you want to provide default values or check for a property's existence. The disadvantage is that the bug is silent — your code continues executing with a bad value, and the actual error might show up far from the original mistake, making it much harder to debug.

Question 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
Show answer
(a) 5 — the minus operator only works on numbers, so "10" is coerced to 10 (coercion, but produces the expected result). (b) "105" — the plus operator sees a string and concatenates (coercion bug). (c) 2 — true is coerced to 1, so 1 + 1 = 2 (coercion). (d) true — loose equality treats null and undefined as equal to each other (coercion issue; using === would return false, which is usually the safer choice).

Question 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".
Show answer
(a) It throws TypeError: Cannot read properties of null (reading 'background'). Even though config.theme.colors exists, its value is null, and you cannot access a property on null — just like undefined. This is an undefined-access-style bug. (b) The fixed version is: const bg = config.theme.colors?.background ?? "white"; The optional chaining (?.) returns undefined if colors is null, and the nullish coalescing operator (??) provides the default "white".

Question 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; }
Show answer
This is an async timing bug. The fetch() call is asynchronous, so the line that updates the display runs immediately — before the fetch completes. The variable temp is still "Loading..." at that point. The fix is to move the display update inside the .then() callback, or better yet, use async/await: async function showWeather() { const res = await fetch('/api/weather'); const data = await res.json(); document.getElementById('display').textContent = data.temperature ?? 'N/A'; } Now the function pauses at each await until the data arrives, and the display update happens after the temperature is available.

Question 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.
Show answer
Two dangerous examples: (1) 0 == '' returns true, which means an empty input field (string) would be treated as equal to zero (number). In a form validation context, this could let invalid data through. With ===, this correctly returns false. (2) false == '0' returns true because both coerce to 0. If you are checking whether a user entered a valid response, this could accept '0' as equivalent to false, breaking your logic. With ===, the comparison correctly returns false because a boolean is not a string. One rare intentional use of == is checking value == null, which is true for both null and undefined but false for everything else. This is a known shorthand that some developers use deliberately to check for 'no value' without writing value === null || value === undefined. However, this should be a conscious choice, not a default habit.

SUMMARY

Lesson Summary

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)