luaguides

error

error raises a runtime error, stopping normal execution and propagating up the call stack until a protected context catches it. It’s how you signal that something has gone wrong and let the caller decide how to handle it.

Signature

error(message [, level])

Arguments:

  • message — the value to raise, typically a string but can be any Lua type
  • level — which call stack level to blame in the failure location (default: 1)

Returns: Nothing — error never returns normally.

Basic Behavior

error stops execution of the current function and propagates the error upward:

function divide(a, b)
    if b == 0 then
        error("division by zero")
    end
    return a / b
end

divide(10, 0)
-- stdin:1: division by zero
-- stack traceback:
--    [C]: in function 'error'
--    stdin:1: in function 'divide'
--    ...

The divide example above terminates the program because there’s no pcall to catch the error — it propagates all the way up and hits the default error handler, which prints the traceback and exits. In real programs, you typically wrap risky calls in pcall so you can inspect the error and recover gracefully instead of crashing:

local ok, err = pcall(function()
    error("something went wrong")
end)

print(ok)  -- false
print(err) -- something went wrong

The message can be any value

Most Lua code passes a string to error(), but the function isn’t picky — it accepts any Lua value as the message. This is useful when a simple string isn’t enough to describe what went wrong. Passing a table, for instance, lets you attach error codes, context data, or a list of validation failures. Whatever you pass becomes what pcall and xpcall return as the second value, so the caller can inspect it directly:

error(42)           -- raises the number 42
error({code = "E1"}) -- raises a table

-- pcall receives whatever you pass:
local ok, val = pcall(error, {code = "E1"})
print(val.code)  -- E1

In practice, almost all error messages are strings. But raising tables as error objects is useful when you need structured error data — error codes, metadata, stack context — that the handler can inspect rather than parse from a string. You can also attach a custom __tostring metamethod to an error table, giving you both structured data for programmatic inspection and a readable string for log output and tracebacks.

The level parameter

level controls which call site appears in the error’s location information. Changing the level doesn’t affect program behaviour — it’s purely cosmetic — but getting it right makes debugging dramatically easier. When a failure fires, the traceback shows a file name, line number, and stack of function calls. The level argument decides which line gets blamed as its origin.

Level 1 (default): blames the line where error was called.

function foo(n)
    if n < 0 then
        error("n must not be negative")  -- blames this line
    end
end

Level 2: shifts the blame up one stack frame so the error points at the caller of the current function, not at the error() call itself. Use this when you’re writing argument validation helpers — the person calling your function should see their own invocation site in the traceback, not a line deep inside your validation logic. This makes debugging far easier because the error location actually matches where the mistake happened:

function foo(n)
    if type(n) ~= "number" then
        error("number expected, got " .. type(n), 2)
    end
end

foo("hello")
-- stdin:1: number expected, got string
-- (points to the call site, not to line 3 of foo)

Level 0: suppresses the location prefix entirely, so the output prints with just the message and no file/line reference. This is the right choice when the message already tells the user everything they need to know — for example, a missing configuration file path or a network timeout. Adding a line number pointing into library internals would only distract from the clear explanation you’ve already written:

error("config file not found", 0)

Common Patterns

Once you understand the mechanics of error() and its level parameter, the real skill is knowing when and how to apply it in your own code. The patterns below cover the most frequent real-world uses. Each one makes a different tradeoff between convenience for the function author and clarity for the caller reading error messages.

Argument Validation

The most common use of error is validating function arguments at the top of a function — catching bad input before it causes a harder-to-debug failure deeper in the call stack:

function set_name(obj, name)
    if type(name) ~= "string" then
        error("name must be a string", 2)
    end
    if #name == 0 then
        error("name cannot be empty", 2)
    end
    obj.name = name
end

Guard Clauses

Argument validation checks the type and shape of inputs, but sometimes you need to reject a function call based on higher-level conditions — missing required fields, invalid combinations of arguments, or violated invariants. Guard clauses put these checks at the very top of a function, using error() to bail out before any real work begins. This keeps the happy path clean and the failure cases explicit:

function process(data)
    if not data then
        error("data is required")
    end
    if not data.url then
        error("data.url is required")
    end
    -- proceed with processing
end

Raising structured errors

Sometimes a single error message isn’t enough — you need the caller to know exactly what failed and why. Raising a table instead of a string lets you bundle an error code, a list of validation problems, or diagnostic context into a single object that pcall delivers intact. The calling code can then inspect fields like .code or .errors to decide how to respond, rather than parsing a string. This pattern works particularly well for public API functions where different callers need different levels of error detail:

function validate_config(cfg)
    local errs = {}
    if not cfg.host then table.insert(errs, "host is required") end
    if not cfg.port then table.insert(errs, "port is required") end
    if #errs > 0 then
        error({
            code = "INVALID_CONFIG",
            errors = errs
        })
    end
end

local ok, err = pcall(validate_config, {})
if not ok and type(err) == "table" then
    print(err.code)      -- INVALID_CONFIG
    print(err.errors[1]) -- host is required
end

How error Interacts with pcall and xpcall

error doesn’t return normally — it performs a longjmp-style stack unwind that tears down every active function call until it finds a protected context. The nearest active pcall or xpcall catches the error and resumes execution from there. If no protected call exists, the default error handler prints the traceback and terminates the program. Understanding this unwinding behaviour is essential because it means you can place a single pcall high in your call stack and have it catch errors from any depth below:

pcall(function()
    error("oops")
end)
-- returns false, "oops"

xpcall(function()
    error("oops")
end, debug.traceback)
-- returns false, "stack traceback:\n  ..."

xpcall(function()
    error("oops")
end, function(err)
    return err .. " (caught by handler)"
end)
-- returns false, "oops (caught by handler)"

When a protected call is active somewhere in the call stack, error is essentially a controlled jump — the stack unwinds, cleanup happens, and the program continues. But if you call error() at the top level or inside a coroutine with no enclosing pcall, there’s nowhere to jump to. Lua’s default error handler takes over, prints the error message and a full stack traceback to stderr, and the process exits. This is the same behaviour you get from an uncaught exception in most languages:

error("fatal")
print("this never runs")
-- program terminates with error message

Common Pitfalls

Even experienced Lua developers stumble over a few subtle behaviours of error(). The pitfalls below are the ones that cause the most debugging headaches — getting the level wrong, passing nil as the message, and abusing error for normal control flow.

Forgetting the level when validating

If you validate arguments and use the default level, the error points inside your function rather than at the caller. The person who passed bad data sees a line number in your library code instead of their own invocation site:

function foo(n)
    if type(n) ~= "number" then
        error("need a number")  -- default level=1
    end
end

foo("x")
-- stdin:3: need a number  (refers to the error() call inside foo)
-- Caller has no idea which argument was wrong

Always pass level=2 in validation functions so the error blames the caller, not your validation code. This small habit saves the person debugging the error from having to read your source to figure out which argument was wrong.

Raising nil

If you pass nil to error, Lua replaces it with a default message instead of raising nil as the error value. This substitution prevents confusing nil-is-error outputs but also masks the real problem — a nil argument to error() is almost always a logic bug where some value failed to initialise:

pcall(error, nil)
-- err = "error object is not a string"

This happens because Lua’s internal error-handling machinery expects error objects to be strings, tables, or other non-nil values. Passing nil is almost always a bug — it means something failed to initialise or a lookup returned nothing.

Using error for control flow

error is for exceptional states, not normal control flow. If a condition is expected, use a return value instead:

-- Wrong:
function find_item(list, key)
    if not list[key] then
        error("key not found")  -- this happens regularly
    end
    return list[key]
end

-- Better:
function find_item(list, key)
    if not list[key] then
        return nil  -- expected outcome, handle with if/else
    end
    return list[key]
end

See Also