Where errors show up

A game that breaks rarely says so on screen. The preview stays empty, the ship does not move, or rocks stop coming, and the actual message is in the browser console. Open it first, every time something looks wrong; most of the work of debugging is getting the right message in front of you.

  • CodePen 2.0 has its own console panel. CodePen's documentation notes that it is "always in the correct context": it shows what happens in the preview. It is less powerful than the browser's own tools, but it is the right place to start.
  • The browser's DevTools (F12, or Cmd+Option+I on a Mac) show the full picture, but in CodePen they start in the context of the codepen.io page itself, not of the preview frame where your game runs. Switch the context to the preview first; see browser DevTools.
  • The Classic editor has a Console button at the bottom of the editor. JavaScript errors are logged there, but according to its documentation not all console commands are supported; many simply map to console.log().

Most consoles show errors in red: something failed and the script stopped at that point. Warnings are usually yellow: the code still runs, but something is outdated or suspicious. Both are worth reading, and the first red line usually matters most, because later errors are often a consequence of the first (see the first error).

Reading the error before you paste it

An uncaught exception is an error that no try...catch handled: it travels up through every function call, stops the script that was running and ends up in the console, usually with "Uncaught" in front of it in a browser. In a game, an error during setup means the picture never appears. An error inside the animation loop usually repeats: Three.js has already asked for the next frame, so the same red line comes back frame after frame, and whatever came after the failing line in that frame (often the drawing) is skipped.

Here is one you could meet in step 3. The game keeps a pool of rocks and launches a free one every 0.7 seconds; the reference version never lets more than 14 of its 30 rocks fly at once. Suppose you asked for "many more rocks" and the answer set the spawn interval to 0.1 seconds and dropped that limit. A rock takes about 4 to 6 seconds to fly past, so after a few seconds the game wants more than 30 rocks in the air, and the pool runs dry. (At the reference interval of 0.7 seconds at most about 9 rocks are in flight, so this cannot happen there.) The example shrinks the pool to three rocks, all in flight, and keeps freeOne exactly as in the reference code:

// The pool from step 3, shrunk to three rocks, all in flight.
const rocks = [{ active: true }, { active: true }, { active: true }];
const freeOne = (pool) => pool.find((item) => !item.active);

function launchRock(rock) {
  rock.active = true;
}

console.log("Launching a rock...");
launchRock(freeOne(rocks)); // no "at most 14 in flight" check
console.log("Launched.");
Launching a rock...
TypeError: Cannot set properties of undefined (setting 'active')

Before pasting anything, read the message in three parts:

  • The type. A TypeError means a value was used in a way its type does not allow, here as an object when it was undefined.
  • The message. "Cannot set properties of undefined (setting 'active')": something tried to write .active on a value that does not exist. Only one line writes active on a rock, so the rock itself was missing: freeOne found no free rock and returned undefined.
  • The place. The stack trace under the message names the function and line (in this example launchRock; in the reference code switchOn, called from launchRock), and the line that called it.

You now know more than "it doesn't work", and you can check whatever the AI suggests: a fix that only hides the error, such as if (!rock) return; inside launchRock, stops the crash, but the real question is why the pool ran out. (Chrome and Node.js word this message the same way because they share a JavaScript engine; Firefox and Safari word it differently, with the same meaning. More on reading messages: reading error messages.)

A worked example: the error that was not in the code

While the game was being tested, one browser showed an empty game. The console showed this (shortened):

THREE.WebGLRenderer: A WebGL context could not be created. Reason: … GL_VENDOR = Disabled …

Read it the same way, part by part:

  1. Who is speaking? THREE.WebGLRenderer: the Three.js renderer, which is the very first thing the game creates. Nothing of the game itself (stars, ship, rocks) had run yet.
  2. What failed? "A WebGL context could not be created": the renderer asked the browser for a WebGL drawing surface, and the browser refused.
  3. Why? The text after "Reason:" comes from the browser, not from Three.js. "GL_VENDOR = Disabled" says the browser was not using the graphics hardware at all: its graphics (hardware) acceleration was switched off or blocked.

So the cause was the browser's graphics, not the code: the same files run fine in a browser with graphics acceleration switched on. Pasting this error into a chat with "fix it" would have invited changes to code that had nothing wrong with it, the first round of a fix loop. What helps is a check outside the code: a WebGL test page such as get.webgl.org, turning graphics acceleration back on in the browser settings, and restarting the browser.

After that message Three.js gives up with an error of its own, the script stops, and without help the game frame on this site would stay empty. The frame now catches the error and says so instead:

WebGL is not available in this browser, so the game cannot draw. Check get.webgl.org, and turn on graphics (hardware) acceleration in the browser settings, then restart the browser.

Two habits come out of this. First, ask which part of the system is speaking before you assume the problem is in your code. Second, when you do involve the AI, ask for a diagnosis rather than a change:

Explain only; do not change the code.

My Three.js game (CodePen, WebGLRenderer) shows nothing, and the console says:
THREE.WebGLRenderer: A WebGL context could not be created. Reason: [the full reason text from the console]

Is this caused by my code, by my browser or by my computer? What can I check to find out, starting with the quickest check?

The fix loop

In the post that named vibe coding, Andrej Karpathy described his approach to errors: "When I get error messages I just copy paste them in with no comment, usually that fixes it", and "Sometimes the LLMs can't fix a bug so I just work around it or ask for random changes until it goes away" (post on X, 2 February 2025). For a throwaway weekend project, which is what he was describing, that can be good enough. As a habit, it has a name.

A fix loop is the cycle of pasting an error into an AI assistant, accepting the suggested change without understanding it, and pasting the next error, round after round, while the code grows and the actual cause stays unknown. It happens for understandable reasons:

  • The model cannot see your screen. It only knows the error text and the code you pasted, so it guesses at the rest (see How an AI Model Writes Code).
  • Each fix adds something. A check here, a workaround there; fixes that did not help usually stay in, and the next error may come from one of them.
  • The conversation gets long. Earlier versions of the code and abandoned ideas stay in the context and compete with the current version (see How a Model Reads Your Prompt).
  • Hiding an error looks like fixing it. When the red line disappears, it is tempting to stop asking whether the game now does the right thing.

The rest of this page is about the alternatives: better input for each fix, narrowing down the problem yourself, and knowing when to stop and go back.

Giving the error and its context

Error context is the information around an error that a helper needs to diagnose it: the exact message and stack trace, the code it points to, what you did just before, what you expected, and what has changed since the last time it worked. It is what turns a paste into an error-message prompt, and it is the same information a colleague would ask for.

my game doesnt work anymore. fix it
My CodePen game (Three.js, three files) worked until your last change, which made rocks spawn every 0.1 seconds and removed the "at most 14 rocks in flight" check.

What I did: I played for a few seconds.
What happened: the console shows this error, over and over:
Uncaught TypeError: Cannot set properties of undefined (setting 'active')
    at switchOn (script.js)
    at launchRock (script.js)

What I expected: rocks keep coming forever, recycled from the pool of 30.

Here are switchOn, launchRock, freeOne and updateRocks:
[paste the four functions]

First explain the cause in two sentences. Then show the smallest change that fixes it. Do not change anything else.

The second prompt says what changed last, which is often the single most useful fact, and it asks for the cause first, so you can judge the diagnosis before you look at any code.

Asking for the smallest fix

A minimal fix request is a debugging prompt that asks for the smallest change that removes the bug and explicitly rules out other changes, so the answer stays small enough to review and test. Without it, a debugging answer often comes back as a rewritten function with a few unrequested "improvements", and one of those may cause the next bug.

Cause: a new rock now comes every 0.1 seconds but each one flies for several seconds, so more than 30 are needed at once. Once all 30 are active, freeOne() finds no free rock and returns undefined, and switchOn() then tries to set .active on undefined.

Smallest fix, in updateRocks():
  if (spawnTimer <= 0 && activeIn(rocks).length < ROCK_MAX) {

An answer like this one is easy to check: you can see the one line it changes, and the explanation matches what you read in the error yourself. Ask for the whole file only after you have accepted the fix, if pasting a fragment is awkward (see Prompting in Small Steps).

Try it yourself

Open a new chat in an AI assistant and paste the first prompt. Then start another new chat and paste the second. A new chat per prompt keeps the first answer from influencing the second. The code is a small fictional example.

My game doesn't work, fix it.

const scores = [];

function bestScore() {
  return scores.reduce((best, score) => Math.max(best, score));
}

console.log("Best: " + bestScore());
JavaScript in the browser. bestScore() should return the highest score so far, or 0 when no game has been played yet. Before the first game, the console says:
Uncaught TypeError: Reduce of empty array with no initial value

Explain the cause in two sentences, then show the smallest change that returns 0 for an empty list. Do not change anything else.

const scores = [];

function bestScore() {
  return scores.reduce((best, score) => Math.max(best, score));
}

console.log("Best: " + bestScore());

What to look for: whether the first answer finds the empty-list problem at all or guesses something else; how much each answer changes that you did not ask for (new functions, renamed variables, a different way of storing scores); whether the second answer's fix touches only the empty case; and which answer you could check in a minute.

Isolating the problem

Bug isolation is narrowing a bug down to the smallest piece of code, input and set of steps that still shows it, so the cause has few places left to hide. It is useful whether or not you ask an AI afterward: a small, reliable case is the best minimal reproducible example for any helper, human or model.

  • Make it happen on purpose. "Sometimes the game ends for no reason" is hard; "a rock passing 2 units to the left of the wing tip ends the game" can be tested.
  • Switch half of it off. Comment out half of the new code or one feature (the rocks, the ship's tilt). If the bug stays, it is in the other half. This halving finds the cause in a few rounds, even in a long script.
  • Check one assumption at a time. Is the function called at all? With which values? Does the value change the way you think it does?
  • Explain it out loud. Describing the code line by line, to a colleague or to nobody, often shows the gap (see rubber duck debugging).

Debug logging

Debug logging is adding temporary output statements, such as console.log(), that print values and events while the program runs, to compare what the code really does with what you expect; they are removed once the bug is found. In a game there is one catch: the loop runs 60 or more times per second, so a log line in every frame floods the console. Log when something happens (a shot, a hit, a state change), not every frame, and give each line a recognizable prefix.

The pure functions at the top of the game's script make this easy to do outside the browser too. burstStep below is copied from step 5 of the game, with the three constants it uses; the loop underneath pretends to hold the trigger for two seconds at 60 frames per second and logs every shot:

const BURST_SIZE = 3; // shots per burst
const BURST_GAP = 0.08; // seconds between the shots of a burst
const BURST_PAUSE = 0.35; // seconds before the next burst

// The burst rhythm, one frame at a time. burst = { left, wait }: shots
// left in this burst and seconds until the next may fire. Returns the
// new burst and whether to fire now.
function burstStep(burst, trigger, dt) {
  const wait = Math.max(0, burst.wait - dt);
  if (wait > 0 || (burst.left === 0 && !trigger)) {
    return { left: burst.left, wait, fire: false };
  }
  const left = (burst.left === 0 ? BURST_SIZE : burst.left) - 1;
  return { left, wait: left > 0 ? BURST_GAP : BURST_PAUSE, fire: true };
}

// Debug logging: hold the trigger for 2 seconds at 60 frames per second.
const dt = 1 / 60;
let burst = { left: 0, wait: 0 };
let shots = 0;
for (let frame = 0; frame < 120; frame++) {
  burst = burstStep(burst, true, dt);
  if (burst.fire) {
    shots++;
    console.log(`[debug] frame ${frame}: shot ${shots}, ${burst.left} left`);
  }
}
console.log(`[debug] ${shots} shots in 2 seconds`);
[debug] frame 0: shot 1, 2 left
[debug] frame 5: shot 2, 1 left
[debug] frame 10: shot 3, 0 left
[debug] frame 32: shot 4, 2 left
[debug] frame 37: shot 5, 1 left
[debug] frame 42: shot 6, 0 left
[debug] frame 64: shot 7, 2 left
[debug] frame 69: shot 8, 1 left
[debug] frame 74: shot 9, 0 left
[debug] frame 96: shot 10, 2 left
[debug] frame 101: shot 11, 1 left
[debug] frame 106: shot 12, 0 left
[debug] 12 shots in 2 seconds

Twelve lines tell you more than any description: three shots five frames apart, then a longer pause, four times. If the game "fires too fast" or "skips shots", a log like this shows whether the rule or something around it is to blame.

Logging also catches bugs that no amount of reading finds. While the game was being built, a test of an earlier design found a cooldown that sometimes refused a shot it should have allowed. The logic was right; the arithmetic was not exact:

// A shot is allowed once COOLDOWN seconds have passed since the last one.
const COOLDOWN = 0.2;
const lastShot = 0.8;
const now = 1.0;

console.log(now - lastShot);
console.log(now - lastShot >= COOLDOWN ? "fire" : "wait");
0.19999999999999996
wait

Computers store most decimal fractions in binary only approximately, so 1.0 - 0.8 is a hair less than 0.2 and the comparison fails (see floating-point precision and comparing floats). An AI asked "is this logic correct?" may well say yes, because the logic is. A single console.log of the difference settles it in a second.

When fixes pile up, go back

Revert and retry is the debugging move of throwing away a series of failed fixes, returning to the last version that worked, and asking again with what you have learned, usually for a smaller change or in a different way. It feels like losing work. In practice the failed fixes are rarely worth keeping: you no longer know which of them matter, and some may be causing the next problem.

A practical rule: when two fixes in a row did not solve the problem, stop. Go back to your known-good version (the copy you saved before the step; see Saving Versions and Going Back), start a new chat, and give it the clean code, the goal and what you learned from the failed attempts.

The fix loop and the way out. On the left, the loop: an error appears, it is pasted with "fix it", the changed code produces a new error, and the loop starts again. After two failed fixes, an arrow leads to the right: go back to the known-good version, start a new chat with the error, the context and what you already tried, and ask for one smaller change, then check it. Error in the console Paste: "fix it" Changed code, a new error the fix loop after two failed fixes Back to the known-good version New chat: the error, the context and what you already tried One smaller change, then check
The loop has no exit of its own; going back to a version that worked gives you one.
I am building a small Three.js game in CodePen, step by step. Below is my last working version (all three files).

I want to add: [the goal of the step, in one or two sentences].

I tried this before in another chat. Two attempts failed:
- The first version made the game run at double speed after a restart.
- The second fixed that but left old rocks on screen.

Make only this change, keep everything that already works, and tell me which functions you changed.

[paste index.html, style.css and script.js here]

Telling the model what went wrong before saves you from getting the same failed approach a second time, and the clean code means it no longer has to work around its own earlier patches.

Errors you may meet in the game

Some problems come up again and again in AI-written Three.js code for CodePen. The table lists the ones from the build steps, with where to look first.

What you see Likely cause What to do
"Cannot use import statement outside a module", or "THREE is not defined" The script tag lacks type="module", or the line import * as THREE from "three"; is missing or changed. Check the tag in index.html and the first line of the script (see Build 1).
A warning: Clock: This module has been deprecated. Please use THREE.Timer instead. The AI used THREE.Clock, which is deprecated since Three.js r183 (an outdated habit from older code). Ask for THREE.Timer with connect(document); the console may show the warning with a prefix.
The keys do nothing The preview does not have keyboard focus. Click in the game once, then use the keys.
An empty game and "A WebGL context could not be created" The browser's graphics, not the code. See the worked example.
Everything runs at double speed after a restart A second animation loop was started. Start the loop once, at setup; a restart should only reset values (see Build 4).
No sound, and no error Browsers keep audio silent until the player has pressed a key or clicked. Start the sound after the first key press; see Build 7.