Create: Computing AgesBrass Docs
The Brass language

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:

Terminal
> door
Opening the north gate...
door:12: attempt to index a nil value (local 'lamp')
>

The message has three parts, separated by colons:

  1. The file: door. A file in a folder shows its full path (apps/door/main:12:), a line typed at the brass prompt is called brass, and on this site the examples are called snippet. When a program is split into several files with import(), this tells you which file to open.
  2. The line: 12. Type edit door: the editor shows line numbers in the margin.
  3. 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 variable lamp was nil.

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.

Watch out

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.

Brass
local level = 12
if level > 10 then
  print("too high")
Screen
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.

Brass
local text = fs.read("settings")   -- nil: there is no such file
print(text:upper())
Screen
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:

Brass
local settings = {station = "Iron Mine"}
print(settings.alarm.side)
Screen
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 nil when it finds nothing: fs.read (no file), peripheral.wrap and peripheral.find (no block there), net.receive (time out), string.find (not found);
  • a typo in a field name (settings.Station is not settings.station);
  • a library this computer does not have: gfx on the Tube Computer, net before the Minicomputer. The message is then attempt to index a nil value (global 'net').

Fix: test before using, and say what is missing.

Brass
local lamp = peripheral.wrap("top")
if lamp == nil then
  error("no lamp on top of the computer")
end

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

Brass
term.setCursorPos(1, 1)
Screen
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:

Brass
greet("Steve")
function greet(name)
  print("Hello, " .. name)
end
Screen
snippet: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.

Brass
local stock = {iron = 120}
print(stock.iron + stock.gold)
Screen
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.

Brass
local stops = {"Iron Mine", "Brass Works"}
print("Next stop: " .. stops[3])
Screen
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:

Brass
local level = "12"   -- read from a file: it is text
if level > 10 then
  print("too high")
end
Screen
snippet: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:

Brass
local typed = "3.7"
print(math.floor(typed))
Screen
snippet:2: bad argument #1 to 'floor' (number expec
ted, got string)

A missing argument, or a nil one, shows as got no value:

Brass
print(string.rep("-"))
Screen
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

messagecausefix
attempt to get length of a nil value#x where x is nilcheck the table exists
attempt to index a string value with a number keyname[1] to get a charactername:sub(1, 1)
attempt to index a number valuex.field or x:method() on a numberthe variable does not hold what you think: print it
table index is nilt[key] = value with key being nilcheck the key
'for' limit must be a numberfor i = 1, count with count being nil or texttonumber, or a default
attempt to iterate over a nil valuefor v in list with list being nilcheck the list
use pairs() or ipairs() to iterate a table with two variablesfor k, v in tfor k, v in pairs(t)
an iterator can only be used in a 'for' loopcalling pairs(t) outside a foruse it in a for
string too longa text over 65536 characterscut it, or write it in several files
no storage mediumfs with no disk in the computerinsert a medium
not enough spacethe medium is fulldelete 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.

Brass
local function depth(n)
  return depth(n + 1)  -- no condition to stop
end
depth(1)
Screen
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:

Brass
local history = {}
local n = 0
while true do
  n = n + 1
  history[n] = "reading " .. n   -- nothing is ever removed
end
Screen
snippet: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.

Brass
for i = 1, 300 do
  os.start_timer(60)
end
Screen
snippet: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:

messageusual 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 stringa 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 expressionf() = 3: only variables and fields take a value
'break' outside a loopbreak 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:

messagewhat to do
cannot capture local 'count' of an enclosing functionmove 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 supportedtake a table parameter
unexpected symbol '!' (use 'not')not x; != is accepted, ! alone is not

A table where two fields have no comma between them:

Brass
local press = {
  speed = 64
  stress = 2
}
Screen
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.

Brass
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)
Screen
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:

Brass
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")
Screen
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 = ...} when f worked (value is what f returned);
  • {ok = false, error = "..."} when it failed (error is the message, position included).
Brass
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)
Screen
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.

Tip

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.

Brass
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)
Screen
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:

Brass
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)
end

Cleaning 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:

Brass
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)
end
Screen
press 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.reboot and os.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.

The simplest tool is the best one: print the values just before the line that misbehaves, with a label and with their type.

Brass
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)
Screen
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:

Brass
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})
Screen
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:

Brass
local DEBUG = true

local function trace(message)
  if DEBUG then
    print("[debug] " .. message)
  end
end

trace("vault has " .. 1250 .. " iron")
Screen
[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):

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

Terminal
> 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, or os.pull_event("key") to go one step per key press. A slower shaft also slows the computer down (see The computers).
  • Check your assumptions at the top of a function, so that a wrong value fails early, close to its cause:
Brass
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)
end

When something goes wrong: a checklist

  1. Read the file and the line, and open them with edit.
  2. Read the name in parentheses: it is the value that was not what you expected.
  3. print the values used on that line, with their type.
  4. Try the expression at the brass prompt.
  5. 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