Errors and debugging
Read an error message, fix the most common ones, raise and catch your own errors, and track bugs down with traces, log files and the brass prompt.
Every program goes wrong at some point: a typo, a chest that was moved, a number read as text. In Brass an error never harms the computer or the world. The program stops, and a short message says where and why. This guide teaches you to read those messages, to fix the usual suspects, to raise and catch your own errors, and to find the bugs that do not announce themselves.
Reading an error message
When a program hits an error, it stops at once. The message is printed in red under what the program wrote, and the prompt comes back:
> door Opening the north gate... door:12: attempt to index a nil value (local 'lamp') >
The message has three parts, separated by colons:
- The file:
door. A file in a folder shows its full path (apps/door/main:12:), a line typed at thebrassprompt is calledbrass, and on this site the examples are calledsnippet. When a program is split into several files withimport(), this tells you which file to open. - The line:
12. Typeedit door: the editor shows line numbers in the margin. - What went wrong:
attempt to index a nil value (local 'lamp'). The part in parentheses names the culprit when Brass knows it: here, the local variablelampwasnil.
Brass shows only the line where the error happened, not the chain of calls that led there. If the line is inside a function called from many places, add a print before the call (see debugging) to learn which call it was.
An error stops the program, not what it already did. A redstone output that was on stays on, a Mechanical Press that was running keeps running. If that matters, catch the error and put things in a safe state (see pcall), or reboot with Ctrl+R: a reboot releases every output.
Compile errors and runtime errors
There are two moments when an error can appear.
Compile errors come before the program starts. Brass first reads the whole file and turns it into instructions for the processor. If the text is not valid Brass (a missing end, a stray symbol, an unclosed string), nothing runs at all, not even the first line. The message says where the compiler got lost, which is not always where the mistake is: a missing end is often noticed only at the end of the file.
local level = 12
if level > 10 then
print("too high")snippet:3: 'end' expected (to close 'if' at line 2) near <eof>
Here the mistake is at line 2 (the if has no end) but the compiler only knew it at the end of the file, on line 3, where <eof> (end of file) came instead of end. The message gives the opening line to help you.
Runtime errors happen while the program runs: everything before the faulty line was done. They depend on the values at that moment, so a program can run fine for an hour and then fail when a chest is empty or a block is broken.
The most common runtime errors
attempt to index a nil value
You read a field (x.name, x[1], x:method()) on something that is nil.
local text = fs.read("settings") -- nil: there is no such file
print(text:upper())snippet:2: attempt to index a nil value (local 'tex t')
The parentheses say which value was nil: local 'text', global 'config', or field 'alarm' when it was the field of a table:
local settings = {station = "Iron Mine"}
print(settings.alarm.side)snippet:2: attempt to index a nil value (field 'ala rm')
settings.alarm does not exist, so .side cannot be read on it. Usual causes:
- a function that returns
nilwhen it finds nothing:fs.read(no file),peripheral.wrapandperipheral.find(no block there),net.receive(time out),string.find(not found); - a typo in a field name (
settings.Stationis notsettings.station); - a library this computer does not have:
gfxon the Tube Computer,netbefore the Minicomputer. The message is thenattempt to index a nil value (global 'net').
Fix: test before using, and say what is missing.
local lamp = peripheral.wrap("top")
if lamp == nil then
error("no lamp on top of the computer")
endattempt to call a nil value
You called something that is not a function: a misspelled name, a function of another API, or a function defined further down.
term.setCursorPos(1, 1)snippet:1: attempt to call a nil value (field 'setC ursorPos')
term has no setCursorPos (that is ComputerCraft's name): Brass names it term.set_cursor. Brass functions are written in snake_case, see Brass for Lua and ComputerCraft users.
The code of a file runs from top to bottom, and function greet() creates greet when that line runs. Calling it earlier fails:
greet("Steve")
function greet(name)
print("Hello, " .. name)
endsnippet:1: attempt to call a nil value (global 'gre et')
Fix: define your functions at the top of the file, and start the work at the bottom. When the name is right and the function exists, check the spelling of its library (string.split, not string.Split), and type help term at the prompt to list what a library has.
attempt to perform arithmetic on a nil value (or a string value)
A +, -, *, /, % or ^ got something that is not a number.
local stock = {iron = 120}
print(stock.iron + stock.gold)snippet:2: attempt to perform arithmetic on a nil v alue
This message does not name the value: look at each operand of the line. Here stock.gold is missing. Give missing values a default with or: stock.iron + (stock.gold or 0).
With a string value, the number is in fact text, usually read with read or fs.read. Brass never converts text to a number by itself: use tonumber, and check for nil (see Working with text).
attempt to concatenate a nil value
The .. operator joins text and numbers only.
local stops = {"Iron Mine", "Brass Works"}
print("Next stop: " .. stops[3])snippet:2: attempt to concatenate a nil value
The same happens with a boolean value ("on: " .. true) and a table value. Fix: tostring(value), or a default: (stops[3] or "end of line").
attempt to compare
<, >, <= and >= work on two numbers or on two strings, never on a mix:
local level = "12" -- read from a file: it is text
if level > 10 then
print("too high")
endsnippet:2: attempt to compare string with number
Other forms: attempt to compare nil with number (a missing value), attempt to compare two table values. Fix: tonumber(level). (== and ~= never fail: "12" == 12 is simply false.)
bad argument
A library function got a value it cannot use. The message gives the position of the argument, the name of the function, what it expected and what it got:
local typed = "3.7"
print(math.floor(typed))snippet:2: bad argument #1 to 'floor' (number expec ted, got string)
A missing argument, or a nil one, shows as got no value:
print(string.rep("-"))snippet:1: bad argument #2 to 'rep' (number expecte d, got no value)
Some functions explain in their own words: (number has no integer representation) when a whole number is needed and you gave 2.5, (color must be 0..15) for term.set_fg, bad side 'up' (front, back, left, right, top, bottom) for rs.set. Every message is listed in Error messages.
Other messages you will meet
| message | cause | fix |
|---|---|---|
attempt to get length of a nil value | #x where x is nil | check the table exists |
attempt to index a string value with a number key | name[1] to get a character | name:sub(1, 1) |
attempt to index a number value | x.field or x:method() on a number | the variable does not hold what you think: print it |
table index is nil | t[key] = value with key being nil | check the key |
'for' limit must be a number | for i = 1, count with count being nil or text | tonumber, or a default |
attempt to iterate over a nil value | for v in list with list being nil | check the list |
use pairs() or ipairs() to iterate a table with two variables | for k, v in t | for k, v in pairs(t) |
an iterator can only be used in a 'for' loop | calling pairs(t) outside a for | use it in a for |
string too long | a text over 65536 characters | cut it, or write it in several files |
no storage medium | fs with no disk in the computer | insert a medium |
not enough space | the medium is full | delete files, or use a bigger medium |
Limits: stack, memory and timers
Three errors come from the limits that keep every computer small and fair. See Limits for all of them.
stack overflow: more than 200 function calls inside each other, nearly always a recursive function that never stops.
local function depth(n)
return depth(n + 1) -- no condition to stop
end
depth(1)snippet:2: stack overflow
Give the function a case where it does not call itself, or rewrite it as a loop.
out of memory: the program keeps more data than the computer's memory holds (from 2 K cells on the Tube Computer to 1 M on the Modern Computer). The usual culprit is a table that grows forever, like a history of readings:
local history = {}
local n = 0
while true do
n = n + 1
history[n] = "reading " .. n -- nothing is ever removed
endsnippet:5: out of memory
Keep only what you need (if #history > 100 then table.remove(history, 1) end), write old data to a file, and watch os.memory().used. The shell command mem shows the memory in use. Speed, memory and limits explains what takes memory.
too many timers: more than 256 timers are waiting at the same time, usually os.start_timer called in a loop that does not wait for them.
for i = 1, 300 do
os.start_timer(60)
endsnippet:2: too many timers
Start a new timer only when the previous one has fired (see Time and timers).
Common compile errors
A compile error stops the program before its first line. The most frequent:
| message | usual cause |
|---|---|
'end' expected (to close 'if' at line 2) near <eof> | a block (if, for, while, function, do) without its end |
'<eof>' expected near 'end' | one end too many |
'then' expected near '=' | if x = 3 then: comparing is == |
'do' expected near 'print' | while or for without do |
unexpected symbol near '=' | a value is missing: local x = = 3, print(1 +) |
syntax error near '+' | a calculation alone on a line: count + 1 instead of count = count + 1 |
unfinished string | a missing closing quote |
'}' expected (to close '{' at line 1) near 'stress' | a missing comma between two fields of a table |
')' expected (to close '(' at line 1) near <eof> | a missing closing parenthesis |
malformed number near '3x' | a letter glued to a number |
invalid escape sequence '\q' | a backslash in a string: write \\ |
cannot assign to this expression | f() = 3: only variables and fields take a value |
'break' outside a loop | break that is not in a for, while or repeat |
ambiguous syntax (function call x new statement) near '(' | a line that starts with (: put ; at the start of it, or join it to the line above |
And the rules where Brass differs from Lua, explained in Brass for Lua and ComputerCraft users:
| message | what to do |
|---|---|
cannot capture local 'count' of an enclosing function | move count to the top level of the file, or pass it as a parameter |
a function returns a single value (return a table instead) | return {x = x, y = y} instead of return x, y |
variable arguments ('...') are not supported | take a table parameter |
unexpected symbol '!' (use 'not') | not x; != is accepted, ! alone is not |
A table where two fields have no comma between them:
local press = {
speed = 64
stress = 2
}snippet:3: '}' expected (to close '{' at line 1) ne
ar 'stress'The compiler expected the table to end after speed = 64, and found the word stress instead: the comma goes at the end of line 2.
Raising your own errors
error
error(message) stops the program with your message, positioned like any other error. Use it when the program cannot go on in a sensible way: a missing device, a setting out of range. A clear message saves time later.
local function set_speed(rpm)
if rpm < -256 or rpm > 256 then
error("speed out of range: " .. rpm)
end
print("speed " .. rpm)
end
set_speed(128)
set_speed(300)speed 128 snippet:3: speed out of range: 300
The position is always the line of the error call (Lua's second argument, the level, is ignored). Give it a string: other values are turned into text, so a table becomes something like table: 0x1b6d3586.
assert
assert(value, message) checks a value: when it is nil or false, it stops with the message (or assertion failed! without one). Otherwise it returns the value, which makes a compact way to read something that must exist:
fs.write("route", "depot,mine,farm")
local route = assert(fs.read("route"), "no route file")
print(#route:split(",") .. " stops")
local times = assert(fs.read("timetable"), "no timetable file")3 stops snippet:4: no timetable file
Catching errors with pcall
pcall(f, arguments...) calls f with those arguments and catches any error it raises. Instead of stopping the program, it returns a table:
{ok = true, value = ...}whenfworked (valueis whatfreturned);{ok = false, error = "..."}when it failed (erroris the message, position included).
local function read_level(text)
local n = tonumber(text)
if n == nil then
error("not a number: " .. text)
end
return n
end
local r = pcall(read_level, "12")
print(r.ok, r.value)
r = pcall(read_level, "twelve")
print(r.ok, r.error)true 12 false snippet:4: not a number: twelve
It catches the errors of library functions too, which is how a program survives a block broken by a player (traffic light removed), a bad value from a sensor or a full disk.
Pass the arguments to pcall after the function, as above. In Lua you would often write pcall(function() return chest.push("right", 1, count) end), but in Brass a function written inside another one cannot use that function's local variables (cannot capture local). pcall(chest.push, "right", 1, count) does the same thing and always compiles.
If the first argument is not a function, pcall does not fail: it returns {ok = false, error = ...}, for example {ok = false, error = "attempt to call a nil value"} for a misspelled function name.
Trying again
A device that is busy, a contraption that is not assembled yet: sometimes trying again a moment later is the right answer.
local tries = 0
local function read_deployer()
tries = tries + 1
if tries < 3 then
error("deployer busy")
end
return 42
end
local function retry(f, times)
for i = 1, times do
local r = pcall(f)
if r.ok then
return r
end
print("try " .. i .. ": " .. r.error)
sleep(0.5)
end
return {ok = false, error = "gave up after " .. times .. " tries"}
end
local r = retry(read_deployer, 5)
print(r.ok, r.value)try 1: snippet:5: deployer busy try 2: snippet:5: deployer busy true 42
Keeping a program alive
A control program that runs for days should not die on the first surprise. Run the real work in a function, catch what goes wrong, note it in a file, put the machines in a safe state, and start again:
local function main()
-- the whole program: read the sensors, drive the outputs...
end
while true do
local r = pcall(main)
if r.ok then
break -- main returned normally: the job is done
end
rs.set("back", 0) -- stop the press
fs.append("crash.log", r.error .. "\n")
print("restarting after: " .. r.error)
sleep(5)
endCleaning up, then stopping
To stop after a failure but put the outputs in a safe state first, catch the error, clean up, then raise it again:
local function run_press()
error("press jammed")
end
local r = pcall(run_press)
print("press switched off")
if not r.ok then
error(r.error)
endpress switched off snippet:8: snippet:2: press jammed
The message gets a second position, because error adds the line where it is called. Both are useful: line 8 is where the program stopped, line 2 where the trouble started. To keep the original message only, print it in red (term.set_fg(term.colors.red)) and end the program with return.
What pcall cannot catch
- Ctrl+T (the Stop button of the terminal) ends the program at once. It is not an error and no code of yours runs after it: the screen is cleared and shows
Terminated. Outputs stay as they were. os.rebootandos.shutdown(and Ctrl+R) end the program too.- A computer without rotation is not in error: it is frozen, and carries on where it was when the shaft turns again.
Debugging techniques
Some bugs do not raise an error: the program runs, but does the wrong thing. Then you have to look inside it.
Print what is happening
The simplest tool is the best one: print the values just before the line that misbehaves, with a label and with their type.
local readings = {"12", "15", "9"} -- as read from a file
local total = 0
for i, r in ipairs(readings) do
print("reading", i, r, type(r))
total = total + tonumber(r)
end
print("total", total)reading 1 12 string reading 2 15 string reading 3 9 string total 36
type(value) answers "nil", "boolean", "number", "string", "table" or "function". Half of all bugs are a value of the wrong type: a number that is text, a table that is nil.
A table prints as table: 0x1b6d3586, which says nothing about its contents. A small function shows them, tables inside tables included:
local function dump(t, indent)
indent = indent or ""
for k, v in pairs(t) do
if type(v) == "table" then
print(indent .. tostring(k) .. ":")
dump(v, indent .. " ")
else
print(indent .. tostring(k) .. " = " .. tostring(v))
end
end
end
dump({station = "Iron Mine", stock = {iron = 120, gold = 8}, open = true})station = Iron Mine stock: iron = 120 gold = 8 open = true
Turn traces on and off with a switch at the top of the file, rather than deleting them:
local DEBUG = true
local function trace(message)
if DEBUG then
print("[debug] " .. message)
end
end
trace("vault has " .. 1250 .. " iron")[debug] vault has 1250 iron
A log file
When the screen is too small, or the bug happens at night while nobody watches, write the traces to a file with fs.append. Each call adds text at the end of the file (and creates it the first time):
local LOG = "debug.log"
fs.delete(LOG) -- a fresh log at each run
local function log(message)
fs.append(LOG, message .. "\n")
end
log("start")
log("vault: 1250 iron")
log("press started")
write(fs.read(LOG))start vault: 1250 iron press started
Read it later with cat debug.log at the prompt, or edit debug.log. Add the time with string.format("[%7.1f] %s\n", os.clock(), message): os.clock() gives the seconds since the computer started.
A log grows with every line: a Punch Card Deck holds 4 KB, a Floppy Disk 64 KB, and a full medium stops the program with not enough space. Delete the log at start, as above, or keep it short.
The brass prompt
Type brass at the prompt to open the interactive interpreter: each line you type runs at once, and its value is printed. It is the fastest way to try an expression, check what a function returns or read a sensor by hand.
> brass Brass 1.0 - type 'exit' to leave. brass> ("minecraft:iron_ingot"):find(":") 10 brass> math.floor(1250 / 64) 19 brass> tonumber("12 items") brass> level = rs.get("left") brass> level 0 brass> exit >
A line whose value is nil prints nothing (tonumber("12 items") above). Each line is a small program of its own, so a local is forgotten at the next line: use global variables (level = ...) to keep values from one line to the next. Errors show as brass:1: .... Ctrl+T or exit goes back to the shell.
Narrowing it down
- Put
print("got here 1"),print("got here 2")... between the steps: the last one printed tells where it goes wrong. - Comment out a part of the program with
--[[and]]to see whether the bug goes away. - Slow the program down to watch it: a
sleep(1)between steps, oros.pull_event("key")to go one step per key press. A slower shaft also slows the computer down (seeThe computers). - Check your assumptions at the top of a function, so that a wrong value fails early, close to its cause:
local function set_lamp(side, level)
assert(type(side) == "string", "side must be text, got " .. type(side))
assert(level >= 0 and level <= 15, "level must be 0 to 15, got " .. level)
rs.set(side, level)
endWhen something goes wrong: a checklist
- Read the file and the line, and open them with
edit. - Read the name in parentheses: it is the value that was not what you expected.
printthe values used on that line, with theirtype.- Try the expression at the
brassprompt. - Look at what the line depends on in the world: a block moved, a chest empty, a disk full, a computer without the right library.
See also
Error messages: every error message, with its cause.pcall(),error(),assert(): the reference of the three functions.Brass for Lua and ComputerCraft users: the compile errors that come from Lua habits.Limits: memory, call depth, timers, string length.