Skip to content

Error conventions

How neoc modules signal failure. Two shapes, when each applies, message format, handling patterns.

Two shapes of failure

(value, err) tuple : On success: value holds result, err is nil. On failure: value is nil, err is a string. Used for recoverable failures — conditions scripts inspect and react to.

Raised error : Calls error(...), propagates until caught by pcall/xpcall. Used for programming errors — script bugs, not runtime conditions.

Per-function shape is documented on its module reference page. A function consistently uses one shape or the other, never both.

When each applies

Tuple return — failure the caller might reasonably recover from: I/O failures (std:fs.read_to_string on missing file), parse failures (vnd:serde_json.from_str on malformed input), network failures (vnd:hyper.get on unreachable host).

Raise — script misused the surface: wrong argument type, method on closed handle, non-Sendable in shared table. These are bugs; suppressing them masks the bug.

Error message format

Every error begins with a qualified prefix: "module.function:" (no namespace prefix).

text
fs.read_to_string: No such file or directory (os error 2)
serde_json.from_str: expected value at line 1 column 1
hyper.get: connection refused
thread.spawn: shared.k is 'JsonNull', which is not Sendable. Use args for by-value transfer, or use a Sendable type (Map, Counter, TcpListener).

Consistent across every module. Prefix is matchable programmatically.

Handling failures

Tuple return

lua
local fs = require("std:fs")

local body, err = fs.read_to_string("config.json")
if err then
    return
end
-- use `body`

Same pattern everywhere. No status codes, no exception objects, no wrapper types.

Raised error

lua
local thread = require("std:thread")
local json   = require("lib:json")

local ok, err = pcall(function()
    thread.spawn({ source = "return 1", shared = { v = json.null } })
end)
if not ok then
    -- err contains qualified message
end

Use pcall when genuine recovery from a programming error is needed — test runners, top-level supervisors.

Suggestions and enrichment

When an error involves a name lookup (module, method, field), the runtime provides "did you mean" suggestions using Levenshtein edit distance with threshold max(1, name.len() / 3).

Structure:

text
module.function: what went wrong[ — did you mean "X"?][ available: a, b, c]

Rules:

  • Suggestions appear only when edit distance is within threshold — never garbage matches.
  • Suggestions never echo the user's original input.
  • Available options are enumerated when the candidate set has ≤ 10 items.
  • Type mismatch errors list accepted types: "expected SQL string, got table — accepted: string".

All suggestion computation lives in the error path. Zero cost on the happy path.

CLI diagnostics

Errors that surface through the CLI (not through Lua (nil, string) tuples) are rendered with source context, error codes, and help text using miette. Each error code links to a docs page.

text
  × require: unknown module "lib:jsn"
  help: did you mean "lib:json"?
        available: lib:base64, lib:compile, lib:http, lib:ini, lib:json, lib:mime, lib:test, lib:vcr

  neoc::E0001 — https://neoc.dev/errors/E0001

Error codes are flat-namespaced (neoc::ENNNN), sequential, and stable across versions. See error code reference.

See also