When It Doesn't Work
How to find a bug in a program you wrote — including the ones nothing warns you about.
Sooner or later you will write something that runs and does not do what you meant. The introduction makes the case that an error message is the friendliest thing a computer can hand you, and that is true. This page is about the harder half: what to do once you have read the message and still do not know what is wrong, or when there is no message at all.
People who have written programs for thirty years spend a good part of every week here. What practice changes is not how often you land in it. It is how long you stay.
Start from the one fact that makes all of this possible: the computer did exactly what you told it. It did not misunderstand you, it did not decide to be difficult, and it is not having a bad day. Every single time, the gap is between what you meant and what you actually wrote, and that gap is a real thing sitting in a real file that you can go and look at. Finding a bug is not guesswork or luck. It is narrowing.
This page is about how to narrow. It assumes you have written a little lux — the introduction or a keep of your own from Building Your Own Keep is plenty — and that something of yours is currently broken.
Three kinds of wrong
Before you can fix anything you need to know which of three quite different situations you are in, because they call for different moves and it is easy to waste an hour applying the wrong one.
It will not start. You type lux run and nothing happens except a message. Not one line of your program ran. Something about the way it is written stopped lux from understanding it at all — a missing bracket, a misspelled name, a type that does not fit.
It starts and then stops. Some of it works. You see output, and then it dies partway through with a message. The program was fine as writing but asked for something impossible while it was running, like the fourth item of a three-item list.
It runs all the way through and the answer is wrong. No message. No complaint. The program finishes and quietly tells you something untrue.
The first two are the friendly ones, even though they feel worse, because lux hands you a message with a line number attached. The third is the one that costs real time, and most of this page is about it.
One thing about that first kind is worth knowing early, because it saves you from a whole category of nasty surprise. lux checks that your types fit together everywhere in the file before it runs a single line — including inside branches that will not run this time. So a mistake tucked inside the if that only fires when you lose your last life does not sit there waiting for the day you finally lose it:
var lives = 3
print("You set out with", lives, "lives.")
if lives == 0 {
print("Game over after " + lives + " lives.")
}
error: cannot add a string and an int
--> lives.lux:6:11
6 | print("Game over after " + lives + " lives.")
^^^^^^^^^^^^^^^^^^^^^^^^^^
help: `lux learn strings` — lux never turns a number into text for
you — you ask
lives is 3, so that branch never happens, and the program has no business failing. lux refuses it anyway. Take the useful half from this: if your program started at all, none of its type mistakes are hiding. That is one less thing to suspect when you are down to guessing.
When lux stops you
A message is a gift. It means lux found the problem for you, and all that is left is reading carefully. lux writes every message in the same four parts — what went wrong, an arrow at the spot, sometimes a note: with the specific fact you need, and sometimes a help: pointing at the card that explains the idea behind it.
Here is one of the will-not-start kind. The program is three lines and the last } was never typed:
error: expected '}' to close the block
--> guess.lux:4:1
4 |
^
Look at where the arrow points. Line 4 — and the file only has three lines of code in it. That is not lux being confused. It is the most useful thing on this page, so here it is plainly: the arrow points at where lux noticed, which is not always where you made the mistake. lux read your if, went looking for the } that closes it, and ran out of file. The mistake is up on line 2 where the block opened. The message is where the search ended.
So read the message before you stare at the line. If you only ever change your habits in one way after reading this page, make it that one.
Now the starts-and-then-stops kind:
start
error: index 3 is out of bounds for an array of length 3
--> pack.lux:3:7
3 | print(pack[3])
^^^^^^^
note: valid indices are 0 to 2
help: `lux learn arrays` — the first element is 0, so the last is length minus 1
Notice the word start above the error. That is your program's own output, printed before anything went wrong, and it is a real clue rather than clutter. Everything you saw before the error genuinely happened. The program got at least that far, which means everything above that point is provisionally innocent and you can concentrate on what came after. When you are hunting a bug and the program dies, the last thing it printed tells you where to start looking.
The note: line here is doing the actual teaching. Counting starts at zero, so a three-item array has positions 0, 1 and 2, and there is no 3. Nearly every out-of-bounds error is that same off-by-one, and once it has bitten you twice you will start seeing it coming.
In your terminal: lux learn errors. The slower version, with more examples: When something breaks in the introduction. If you are building a keep, When lux stops you lists the specific messages that come up while you are adding rooms and things.
When the only thing wrong is the answer
This is the hard one. The program runs. Nothing is underlined in red. And it is wrong.
There is no message here because lux has nothing to complain about — you asked for something perfectly possible, and it did it. The problem is that what you asked for is not what you wanted, and no language can tell the difference. That gap is yours to close.
The move that closes it faster than anything else is a question, and it is worth asking out loud because the vague version does not work:
What exactly did I expect this line to do, and what did it actually do?
Most bugs die on the first half of that question. You go to answer it and find you cannot say precisely what you expected — you had a general feeling that this bit puts the coin in the pocket, and when you try to say what "puts" means in terms of the actual values, the fog turns out to be the bug. That is a good outcome, not an embarrassing one. You have just found the exact sentence you were fuzzy about, and fuzzy sentences are where bugs live.
The rest of this page is three ways to answer the second half.
Watch it run
Reading a program tells you what you think it does. lux trace tells you what it does. It runs the program exactly as lux run does, but narrates every line as it goes and shows you what changed on the right-hand side.
Here is a real bug of a kind you will write yourself, probably this week. A door that refuses to open:
struct World {
doorOpen: bool
}
func openDoor(w: World) -> World {
var next = w
next.doorOpen = true
print("The door swings open.")
return next
}
var world = World(doorOpen: false)
openDoor(world)
if world.doorOpen {
print("You walk north.")
} else {
print("The door is locked.")
}
Run it and you get a small contradiction:
The door swings open.
The door is locked.
Both lines are printed by your own program, and they cannot both be right. You could stare at this for twenty minutes. Instead, watch it:
$ lux trace door.lux
tracing door.lux — each line as it runs, with the state it changes on the right
(your program's own output is on stdout; this trace is on stderr)
12 var world = World(doorOpen: false) world = World(doorOpen: false)
13 openDoor(world)
6 var next = w next = World(doorOpen: false)
7 next.doorOpen = true next = World(doorOpen: true)
8 print("The door swings open.")
The door swings open.
9 return next → World(doorOpen: true)
15 if world.doorOpen { (no)
18 print("The door is locked.")
The door is locked.
Read down the right-hand column and the bug is not hiding any more. Line 7: the door opened — next really does become doorOpen: true. Line 9: that new world was handed back, and the arrow shows exactly what came out. Then line 15 asks world whether its door is open and the answer is (no).
So the function worked perfectly and nobody caught what it threw. Line 13 calls openDoor(world) and does nothing with the result, so the new world with the open door was made, returned, and dropped on the floor. The world the if asks about is the old one, which never changed and never could have.
The fix is one word:
world = openDoor(world)
And the same trace, after:
9 return next → World(doorOpen: true)
13 world = openDoor(world) world = World(doorOpen: true)
15 if world.doorOpen { (yes)
16 print("You walk north.")
Line 13 now has something in its right-hand column, because something changed. That column is the trick: a line that changes nothing shows nothing, and a line you expected to change something and did not is your bug. Scan down the right side looking for a blank where you wanted a value.
One practical note. The trace goes to a separate channel from your program's own output, which is why they interleave on screen. If a long trace is drowning your output, send the trace to a file and read it afterwards:
lux trace door.lux 2> trace.txt
Make it smaller
A trace of a whole keep is hundreds of lines and you will not enjoy reading it. When a program gets big enough that watching it is no longer pleasant, stop debugging the program and start shrinking it.
The goal is the smallest program that still does the wrong thing. Not the smallest program — the smallest one that is still broken. Two ways to get there, and both work.
Cut down. Copy your file somewhere safe first, then start deleting from the copy. Take out a whole room, a whole command, everything after the halfway point. Run it. Still broken? Good — everything you just deleted was innocent, and your search just got half as big. Not broken any more? Then the bug is in what you removed, so put it back and delete a different half.
Build up. Open a new file and write the smallest thing that ought to work — the one struct and the one function you suspect, and nothing else. Run it. If it works, add the next piece. The moment it breaks, the piece you just added is the one to look at.
That door program in the last section was not a real game. It is what a three-hundred-line keep looks like after this treatment: one struct with one field, one function, one if. Everything else was cut away because it turned out not to matter, and once it was gone the bug had nowhere left to hide.
The strange bonus is that this often solves the problem before you finish. Roughly half the time the bug reveals itself somewhere in the cutting, because you have to look hard at every line you decide to keep, and one of them turns out not to say what you assumed it said.
When lux is right and you are still surprised
Some things in lux are working exactly as designed and will still surprise you. None of them produces an error, and none of them is a bug — every one is lux working as designed, and doing something other than what you assumed. They are worth reading now rather than meeting cold at eleven at night.
A copy is a copy
When you hand a struct or an array to a function, the function gets its own copy. Changing that copy has no effect whatsoever on yours.
func addItem(items: [string]) {
var copy = items
copy += "torch"
print("inside:", copy)
}
var pack = ["key"]
addItem(pack)
print("outside:", pack)
inside: [key, torch]
outside: [key]
Both lines are telling the truth. Inside the function the torch really was added, to a thing that stopped existing the moment the function ended. This is not a limitation to route around — it is the reason nothing in your program can be changed behind your back, and once you are used to it you will find you trust your own code more. The way to actually change something is to hand the new version back with return, and for the caller to keep it.
Calling a function and dropping the answer
Which leads straight to the bug from the trace section, the single most common one in a keep. Every function in the little keep hands you back a new world, and if you do not catch it, the whole turn evaporates:
openDoor(world) // the door opens in a world you threw away
world = openDoor(world) // the door opens in the world you keep
lux says nothing about the first line, because calling a function and ignoring what it gives you is a perfectly legal thing to do and sometimes exactly what you want. When a command in your keep does nothing at all, look here first.
Whole numbers divide down
Divide two whole numbers and you get a whole number back, with the remainder thrown away rather than rounded:
print(5 / 2) // 2, not 2.5 and not 3
print(7 / 2) // 3
Give the health bar three hearts to split between two players and each of them gets one. If you want the fraction, ask with decimals: 5.0 / 2.0 gives you 2.5.
Decimals are not exact
This one is true in every language you will ever use, and it is worth meeting once so it never frightens you:
print(0.1 + 0.2) // 0.30000000000000004
print(0.1 + 0.2 == 0.3) // false
Nothing is broken. A computer stores decimals in binary, and a third of the numbers you can write in ten fingers have no exact form in two, in the same way that a third written out in decimal never stops. The practical rule is small and easy: never ask whether two decimals are equal. Ask whether the difference between them is tiny, or use whole numbers — count in pennies rather than pounds, or in half-hearts rather than hearts.
Capital letters count
"North" and "north" are two different strings, and comparing them gives false. That is the right answer to the question you asked and rarely the one you wanted. The little keep matches commands exactly, so a player typing North gets told the keep does not understand — which is a real thing to fix, and one of the first improvements worth making to a keep of your own.
Numbers wrap around at the end
Whole numbers in lux are enormous but not infinite, and adding one to the largest of them takes you to the smallest:
var big = 9223372036854775807
print(big + 1) // -9223372036854775808
You are unlikely to meet this by accident. It is here because it is a genuinely famous source of bugs in real software, and because knowing that numbers have an edge is part of knowing what a computer is.
In your terminal: lux learn numbers for how whole numbers and decimals differ, lux learn strings for comparison, and lux learn functions for what a function does and does not get to touch. These are surprises about the language; for the other kind — where every line is valid lux and the world is broken, like a room nobody can reach or a key locked behind the door it opens — see Mistakes lux can't catch.
When you are properly stuck
Sometimes none of it works and you have been going in circles for an hour. Three things help, and the first one sounds ridiculous until the day it saves you.
Explain it out loud. Not in your head — actually out loud, in sentences, from the beginning. Say what the program is supposed to do, then walk the broken part line by line saying what each line does. A person will do if one is nearby, and a wall will do if not. This works, reliably, and it works because it is not really about the listener: saying it in words forces you to be precise where reading lets you skim, and the bug is nearly always sitting in the place you were skimming.
Walk away. Twenty minutes, or overnight if it is late. This is not giving up and it is not laziness. When you have been staring at a piece of code long enough, you stop reading what is on the screen and start reading what you remember writing, and at that point more staring cannot help you. Coming back cold, you read the actual characters again. A surprising number of bugs are solved in the first thirty seconds after a break.
Read it as the machine, not as the author. Go through the broken section one line at a time, in order, believing nothing. Not "this bit adds the coin" but "this line takes the value in copy.coins, adds one, and puts the result back in copy.coins." You wrote it, so you know what it means, and that knowledge is the thing in your way. The machine cannot read your intentions. Read like it.
And when you find it — because you will find it — take the extra thirty seconds to work out why it was wrong rather than just making it right. A bug you understand is one you will recognize on sight next time, and next time will be sooner than you think.
Where to go next
If you have a keep and want to make it bigger, Building Your Own Keep walks through adding a room, hiding something to find, and teaching the world a new rule. If you would rather build something that is yours from the first line, Starting Something of Your Own goes from a blank file to a finished program one runnable step at a time. If you are still filling gaps in the language itself, An Introduction to Programming goes slowly through every piece. And if the bug you just fixed was the third one this week caused by something lux cannot do yet, that is a signal to notice — When lux Feels Small is about what to do on the day the language runs out.