Skip to content
jsworkbench.

THE DEBUG LIBRARY · OPEN TO EVERYONE

An error is a clue.
Let’s follow it.

Find the actual cause, compare a small reproduction, and see the corrected code. No sign-in needed.

Build a debugging habit

40 entries to help you find the cause

runtime

Cannot read properties of undefined (reading 'x')

The expression immediately before .x evaluated to undefined. JavaScript cannot read a property from that value. The problem is usually the missing parent, not the name x itself.

Find the cause. See the fix.
runtime

ReferenceError: x is not defined

The current scope chain has no accessible declaration for x. Check spelling, capitalization, imports, and whether the declaration lives inside a block or function that this code cannot access.

Find the cause. See the fix.
runtime

TypeError: x is not a function

The name x resolved to a value, but that value cannot be called. Parentheses request a function call; a string, object, number, or undefined value does not become callable because its variable name sounds like an action.

Find the cause. See the fix.
runtime

RangeError: Maximum call stack size exceeded

A chain of synchronous calls grew beyond the engine's available stack. Infinite recursion is a common cause: each call starts another before the current call can return.

Find the cause. See the fix.
syntax

SyntaxError: Unexpected token

The parser found a token that does not fit the grammar at that location. A missing comma, unmatched bracket, unfinished string, or stray delimiter can make the next otherwise sensible token look unexpected.

Find the cause. See the fix.
runtime

TypeError: Cannot set properties of null (setting 'x')

The value on the left of .x is null, so there is no object to receive the assignment. With DOM code, a selector commonly returned null because its selector was wrong, the node was removed, or the script ran before the element existed.

Find the cause. See the fix.
async

Unhandled promise rejection

A promise rejected and no rejection handler owned that failure when the runtime checked it. Browsers often display “Uncaught (in promise)” followed by the rejection reason; other runtimes use different wording.

Find the cause. See the fix.
data

NaN appearing where a number was expected

NaN is a special numeric value representing an invalid numeric result. A failed conversion, undefined used in arithmetic, or an operation such as zero divided by zero can produce it. It then propagates through many later calculations.

Find the cause. See the fix.
runtime

Assignment to constant variable

A const binding was assigned another value after initialization. The error normally appears with a TypeError prefix. Increment operators and compound assignments also reassign the binding, so count++ can trigger it.

Find the cause. See the fix.
runtime

`this` is undefined inside a callback/event handler

A regular function received a different receiver from the one you expected. A method call such as counter.read() supplies counter as this, but copying counter.read into a callback variable does not preserve that call form.

Find the cause. See the fix.
data

Array method returns undefined for every item

There are two related symptoms to distinguish. Map collects each callback's returned value. If a braced callback calculates a result but never returns it, the mapped array contains undefined for each visited item.

Find the cause. See the fix.
async

CORS error blocked by policy

The browser is enforcing cross-origin access rules. Your page requested a resource at a different origin, and the response or required preflight did not grant the permission needed for JavaScript to read it. The exact console message includes the origins and the failed policy condition.

Find the cause. See the fix.
data

Cannot convert undefined or null to object

An operation that needs an object was given null or undefined. Object.keys, Object.values, and Object.entries perform this conversion and reject those two values. A nullable API field or missing function argument is a common source.

Find the cause. See the fix.
logic

Infinite loop freezes the tab with no error

The loop keeps running synchronously and never reaches an exit. The main thread cannot process normal input or rendering while that work continues, so the page can appear frozen without producing a JavaScript exception.

Find the cause. See the fix.
browser

querySelector returns null

The selector did not match a node that exists at the moment the query runs. The next property read then throws, often as a failure to read a property of null. The name in the selector is not the bug by itself. Either the document never contained that element, or the script ran before the parser created it.

Find the cause. See the fix.
browser

localStorage value is a string, or storage throws

localStorage and sessionStorage store strings only, scoped to the origin. A number written with setItem is converted to text. Reading it back and adding 1 concatenates instead of doing arithmetic. JSON.parse then throws if the text is not JSON, and JSON.stringify is required when you meant to store an object.

Find the cause. See the fix.
browser

document.cookie did not do what the jar required

document.cookie is not one string you replace. Assigning it sets or updates a single cookie. Reading it returns only the name=value pairs that script is allowed to see, joined together. HttpOnly cookies are missing from that string on purpose, so a session id set by the server may exist and still be invisible to your log.

Find the cause. See the fix.
browser

The Styles pane is editing the wrong box

DevTools shows the element you selected, not the element you meant. Overlapping boxes, a pseudo-element, or an iframe make it easy to edit a rule for a parent while looking at a child. A declaration that is crossed out has already lost the cascade. Changing its numbers does not change the computed box.

Find the cause. See the fix.
browser

The Network tab shows a failed asset, not a CSS mystery

A layout that is unstyled, a missing image, or a blank page is often a request that never succeeded. The Network tab lists each request with a status, a type, a size, and a timing bar. Status 200 means the browser received a response it considers successful. 304 means a cached copy was reused. 404 means this URL had no resource. A failed or blocked row never received a normal status.

Find the cause. See the fix.
browser

The device toolbar and the phone disagree

The device toolbar changes the layout viewport inside DevTools. It does not, by itself, prove what a phone will do. Without a viewport meta element, mobile browsers often lay the page out around 980 CSS pixels and scale it down. A desktop window that you merely made narrow can still have the meta element and look fine, while the phone zooms out.

Find the cause. See the fix.
browser

The page overflows, collapses, or ignores an asset

A horizontal scrollbar, a column that will not shrink, or a hole where an image should be is a layout failure with a small set of causes. content-box width plus padding is wider than the parent. A flex item or a 1fr track will not shrink below its content. An image without a maximum width, or a pre element that does not wrap, forces the page open. A missing stylesheet or image leaves the HTML in place and the presentation half-applied.

Find the cause. See the fix.
logic

Each child in a list should have a unique key prop

React is reconciling an array of children and has no stable identity for each item. Without a key, it falls back to the index. That works only while the list never inserts, deletes, or reorders. After a reorder, React updates the component that stayed in that slot, so state such as a text input stays with the position instead of the item. Duplicate keys are worse: React warns, and the matching becomes ambiguous, so nodes can be reused in the wrong order or appear to duplicate.

Find the cause. See the fix.
logic

Stale state inside an effect or callback

A function closes over the state and props from the render that created it. If that function runs later — a timer, a subscription, or an effect whose dependency list omitted the value — it still sees the old render. The screen can show a new count while the callback logs the count from when the effect last ran.

Find the cause. See the fix.
logic

Maximum update depth exceeded (effect sets state with no dependency array)

useEffect with no second argument runs after every render that committed. If the effect calls a setState that changes state, React renders again, the effect runs again, and the cycle does not stop. The familiar message is “Maximum update depth exceeded.” A dependency array that includes an object or array created during render can cause the same loop, because that value is new every time even when the contents look the same.

Find the cause. See the fix.
runtime

A component renders twice in development

In development, React Strict Mode renders components an extra time, and it runs effects, then their cleanup, then the effects again. The extra pass is a check for impure rendering and missing cleanup. It is not a production double render, and it is not a bug by itself. You see it when a component is wrapped in <StrictMode>, which many toolchains, including the default React template, add around the root.

Find the cause. See the fix.
logic

Conflict markers were committed

This failure is a history problem, not a syntax error. The repository still has the commits, but the labels point at the wrong place or the file still contains the unresolved choice. A teammate who fetches next will build on that mistake. Read the graph before you rewrite anything else. This failure is a history problem, not a syntax error. The repository still has the commits, but the labels point at the wrong place or the file still contains the unresolved choice. A teammate who fetches next will build on that mistake. Read the graph before you rewrite anything else. Conflict markers were committed

Find the cause. See the fix.
logic

A push replaced a collaborator's commits

This failure is a history problem, not a syntax error. The repository still has the commits, but the labels point at the wrong place or the file still contains the unresolved choice. A teammate who fetches next will build on that mistake. Read the graph before you rewrite anything else. This failure is a history problem, not a syntax error. The repository still has the commits, but the labels point at the wrong place or the file still contains the unresolved choice. A teammate who fetches next will build on that mistake. Read the graph before you rewrite anything else. A push replaced a collaborator's commits

Find the cause. See the fix.
logic

The new commit is not on a branch

This failure is a history problem, not a syntax error. The repository still has the commits, but the labels point at the wrong place or the file still contains the unresolved choice. A teammate who fetches next will build on that mistake. Read the graph before you rewrite anything else. This failure is a history problem, not a syntax error. The repository still has the commits, but the labels point at the wrong place or the file still contains the unresolved choice. A teammate who fetches next will build on that mistake. Read the graph before you rewrite anything else. The new commit is not on a branch

Find the cause. See the fix.
logic

The commit landed on the wrong branch

This failure is a history problem, not a syntax error. The repository still has the commits, but the labels point at the wrong place or the file still contains the unresolved choice. A teammate who fetches next will build on that mistake. Read the graph before you rewrite anything else. This failure is a history problem, not a syntax error. The repository still has the commits, but the labels point at the wrong place or the file still contains the unresolved choice. A teammate who fetches next will build on that mistake. Read the graph before you rewrite anything else. The commit landed on the wrong branch

Find the cause. See the fix.
logic

Middleware registered in the wrong order

Express runs app.use functions in the order you registered them. Each one may call next or end the response. If an earlier function sends a status and a body, every function registered after it is skipped, including a logger you thought would always run. The function bodies can be correct and the route can be correct. The bug is only the sequence of app.use calls. A guard that rejects the request before the logger, or a parser registered after the route that needed the parsed body, fails the same way: later code never sees the request.

Find the cause. See the fix.
async

The request hangs because next was never called

A middleware function that neither calls next nor writes a response leaves the request open. Express does not guess that you are finished. The client waits, and this runner stops the process at the three second limit. The route below the middleware is not wrong. It never started. The same hang happens when an async function returns a promise and you forget to call next after the await, or you return early on a branch that does not send a response. The missing call is invisible in the status line because no status was sent.

Find the cause. See the fix.
runtime

Cannot set headers after they are sent

One request gets one response. res.json and res.send write the status line and the body, and then the headers are sent. A later res.status, res.json, or res.send tries to start a second response on a connection that already has one. Node reports that the headers were already sent. The second call is often in a middleware that runs after the route, or in a branch you thought had not already answered. The first write succeeded. The error is the extra write, not a failed database or a bad status code.

Find the cause. See the fix.
data

A query returns no documents because the types differ

MongoDB matches the value you stored, including its type. A field saved as the number 12 does not match a query that asks for the string "12". A field saved as a string id does not match a query that passes a number, and the reverse is the same miss. find returns an empty array. findOne returns null. The collection can still contain the document. The connection worked. The filter compared two values that look alike on the screen and are not equal in the database. Guessing that the collection is empty sends you to the wrong fix.

Find the cause. See the fix.
data

An unindexed query gets slower as the collection grows

A find on a field with no index makes MongoDB read every document in the collection. That plan is a collection scan. On a few dozen documents it still returns quickly, so the handler looks fine and the test that only checks the title stays green. As the collection grows, totalDocsExamined climbs with the number of documents, not with the number of matches. The route still returns the right row. The latency is the bug, and nothing in the JSON body says the scan happened. Teams notice it after the data that made the scan cheap is already gone. A projection that drops unused fields does not change this plan. It only changes what comes back after the documents have been read.

Find the cause. See the fix.
data

A malformed model response crashes the route

A model response can look finished and still fail the contract the route promised the client. The AI-native lesson why-ai-code-looks-right-and-isnt names the same gap: looking right is not the same as matching a rule you own. On the server the failure is a body. choices is missing, content is empty, or the text is not the shape you agreed to send on. Code that reads payload.choices[0].message.content throws, and the framework turns that into a 500 whose body is a stack. Callers see a crash. They do not see a refusal you wrote. Forwarding the provider JSON because it parsed ships that shape, including a blank string, straight to the user. A shape check is not a fact check. It is the minimum that stops the crash and stops you from handing over a body you did not promise.

Find the cause. See the fix.
logic

A missing rate limit lets an endpoint be hammered

An endpoint with no limit accepts every call that reaches it. A script can repeat that call as fast as the network allows. For a cheap read, the damage is load on the process and the database. For a route that calls a paid model, each accepted request can spend money, so the same flood is a cost blowout and not only a slow server. The handler can be correct on one manual request and still be unsafe to leave open. Nothing in the happy-path JSON tells you the route will keep saying yes. A counter that runs after the model call, or after the insert, still does the expensive work and then complains. The refusal arrived too late to matter.

Find the cause. See the fix.
browser

A JWT in localStorage was read by injected script

An access token in localStorage is an ordinary string. Any script that runs on that origin can read it. There is no HttpOnly flag for localStorage, and document.cookie is a different store. When an attacker gets a script onto the page, through an unsanitized title, a compromised script tag, or a dependency that writes HTML, that script calls localStorage.getItem and sends the token to another host. The signature on the JWT does not stop this. The signature proves the identity provider minted the token. It does not control who in the browser is allowed to read the string. The attacker then calls your API as the user until the token expires. A short lifetime limits the window. It does not close the read.

Find the cause. See the fix.
logic

The handler checks login and skips ownership

The route checks that a user is logged in, then loads the record by the id in the path and returns it. Those are different questions. Authentication answers who is calling. It does not answer whether this caller owns this id. An attacker with any valid session changes the id and receives another user's note, invoice, or message. The status is 200 because the session was valid and the document existed. Nothing compared note.owner with the caller. Tests that only sign in as the owner stay green, because the owner's own id really is theirs. This is broken object level authorization, and it is common because the happy path looks finished as soon as the 401 for anonymous users is in place.

Find the cause. See the fix.
logic

Login accepts a password list and confirms which emails exist

A login route with no limit lets an attacker try password lists as fast as the network allows. Hashing the password makes each guess slower and does not cap how many guesses arrive. The same route becomes a username oracle when an unknown email and a wrong password return different statuses or different bodies. The attacker keeps the addresses that exist and spends the rest of the list only on those. Forgot-password does the same job for them if it says mail was sent only when the account is real. The rate-limit lesson on the Node track already built the counter. The failure here is that the counter is not on these two routes, and that the response text answers a question the caller should not get to ask.

Find the cause. See the fix.
data

Passwords were encrypted, so the key decrypts every one

Encryption is reversible on purpose. AES, or any cipher, stores ciphertext that the same key turns back into the password. If the application can decrypt the column to check the password, then anyone who reads the database and the key can decrypt every password. The key is often in the same environment, the same backup, or a debug log that printed the config. Users reuse passwords, so the dump is their other accounts too. Naming the column encrypted, or calling the function encryptPassword, does not make the operation one-way. A hash has no path back to the password. Verification hashes the attempt with the stored salt and compares bytes. A wrong guess fails the compare and never yields the original string.

Find the cause. See the fix.