Synsemadocsv0.6.xENES

Reference

Errors & exit codes

Press Run to see a caught error; edit it (remove the try/recover) to see an uncaught Runtime error: …. The tests below it pin the messages of the errors you are most likely to meet.

errors.syn
-- Doc example: how errors surface — raise, try/recover, and a clean message.
-- Edit this and press Run: remove the try/recover to see an uncaught
-- "Runtime error: x must be >= 0 …" instead.
intent: "doc example: errors"

task risky(x)
    when x < 0
        raise("x must be >= 0, got " + text(x))
    give x * 2

task fails()
    give risky(-1)

-- The message of the error `f` raises, or nothing when it does not raise.
task error_of(f)
    try
        f()
    recover err
        give err
    give nothing

task set_out_of_range()
    let xs be [1]
    set xs[10] to 1

-- run shows the recovery in action:
try
    print("risky(5) = " + text(risky(5)))
    print("risky(-3) = " + text(risky(-3)))      -- this one raises
recover err
    print("caught → " + err)

test "raise fails; try/recover catches the message"
    assert_eq(risky(5), 10)
    let caught be ""
    try
        let bad be risky(-1)
    recover err
        set caught to err
    assert(contains(caught, "must be >= 0"))
    assert_error(fails)
    assert_eq(error_of(() => risky(1)), nothing)

test "Python reflexes get the Synsema form in the message"
    let xs be [1]
    assert(contains(error_of(() => None), "in Synsema: nothing"))
    assert(contains(error_of(() => len(xs)), "in Synsema: length(x)"))
    assert(contains(error_of(() => xs.append(2)), "Synsema has no methods: set xs to append(xs, item)"))
    assert(contains(error_of(() => round(1.234, 2)), "round_to(x, n)"))
    assert(contains(error_of(() => "%d" % 5), "there is no %-formatting"))

test "calls: arity and named arguments"
    assert(contains(error_of(() => risky()), "task 'risky' is missing argument 'x'"))
    assert(contains(error_of(() => risky(1, 2)), "task 'risky' takes 1 argument, got 2"))
    assert(contains(error_of(() => upper("a", "b")), "upper() takes at most 1 argument, got 2"))
    assert(contains(error_of(() => upper(s = "a")), "does not accept named arguments"))

test "type: values do not convert behind your back"
    assert(contains(error_of(() => "id=" + nothing), "Cannot add text and nothing — convert it on purpose"))
    assert(contains(error_of(() => fmt("{a}", {})), "fmt: no value for {a}"))
    assert(contains(error_of(() => int("ff", 16)), "int(x, base = 16)"))
    assert(contains(error_of(() => int("ff", 2.0)), "int(x, base = 2)"))
    assert(contains(error_of(() => int(1.5)), "not a whole number"))
    assert(contains(error_of(() => number("123456789012345678901")), "Use int(x)"))
    assert(contains(error_of(() => bytes(33)), "int_to_bytes(33, size)"))
    assert(contains(error_of(() => 1d == 1.0), "cannot mix decimal and float"))
    assert(contains(error_of(() => length(array(5))), "use size(a)"))
    assert(contains(error_of(() => json_encode({"f": (x) => x})), "the value[\"f\"] is a task, not data"))

test "data: missing keys, indexes out of range or not integers, invalid JSON"
    assert(contains(error_of(() => {"a": 1}["b"]), "Map has no key 'b'"))
    assert(contains(error_of(() => [1][5]), "Index 5 out of bounds (list length 1)"))
    assert(contains(error_of(set_out_of_range), "Index 10 out of bounds (list length 1)"))
    assert(contains(error_of(() => [1, 2][1.5]), "index must be an integer, got 1.5"))
    assert(contains(error_of(() => json_decode("{")), "invalid JSON"))

test "total forms return a fallback instead of raising"
    assert_eq(json_decode("{", nothing), nothing)
    assert_eq(int("abc", fallback = nothing), nothing)
    assert_eq(number("abc", nothing), nothing)
    assert_eq(number("2.5", nothing), 2.5)

Raising & catching§

raise("message")            -- fail deliberately (or re-propagate inside recover)

try
    risky()
recover err
    log "failed: " + err    -- err is the message (text)
    raise(err)              -- RE-PROPAGATE; without it, recover swallows the error

give and stop are not errors — they pass through try/recover. (fail(...) builds an HTTP response, it does not raise.) Test that something raises with assert_error(task). The statement form raise "msg" / raise err (no parens) works too, and a bare raise alone is a parse error with the fix in the message. ⚠️ On the published v0.5.1 and older, raise "msg" without parens was two inert expressions — a silent no-op; on those binaries always raise("msg").

A builtin that can fail on untrusted input often has a total form that returns a fallback instead of raising: json_decode(text, nothing), int(text, fallback = nothing), number(text, nothing).

Errors that name the fix§

A message says what to write, not only what went wrong. The reflexes of other languages are recognized at load time (so the program never starts half-wrong) and answered with the Synsema form:

You wroteThe error says
x = 5`declare with let x be …, change it with set x to … `
x += 1`Synsema has no compound assignment (+=): write set x to x + 1`
return v / def f(x):`return is not a Synsema statement: give <value> returns from a task` / def is not a Synsema statement: a function is task name(params) …``
if / elif / else`if is not a Synsema statement: a condition is when <condition> …`, chain with otherwise when <condition> , the fallback branch is otherwise `
when x > 0:Synsema blocks have no colon
break / continue / pass`leave a loop with stop / skip to the next item by wrapping the rest of the body in when / an empty block is not needed`
except / finally`after the try block write recover err / there is no finally: put the cleanup after the try/recover block`
import json / class P:`import a local module with use "./file.syn" as name / a record type is type Name with typed fields`
a if c else b / c ? a : b`the inline form is when c then a otherwise b `
&& / ;`the logical operators are and / or / one statement per line (no ;)`
f"…" / lambda y: y / [x for x in xs] / xs[1:3]the backtick template / a lambda is (y) => y / no comprehensions: apply(…) … where(…) / no slice syntax: slice(xs, start, end?)
// note (a C-style comment)`comments in Synsema start with -- (//` is integer division)
5--1` -- right after a value: … write 5 - -1, or 5 -- comment `

And at run time: None, len, str, null, dropna… → `Undefined variable: 'None' — in Synsema: nothing; xs.append(2) → Synsema has no methods: set xs to append(xs, item); d.get("a") → … get(m, key, default); round(x, 2) → … round_to(x, n); "%d" % x → there is no %-formatting: use a template`.

Since engine v0.6.46: log(x) → `Undefined variable: 'log' — in Synsema: ln(x) for the natural log, log10(x), log2(x) or log_base(x, b); log "message" (no parentheses) writes a log line (synsema check warns it too); in_units(90, "minutes") → in_units() takes a duration, got number — make one first: in_units(duration(seconds = 90), "minutes"); a named argument that does not exist → unknown parameter 'limt' — did you mean 'limit'?; the parameters are: limit, offset (tasks, lambdas and builtins; found when the call runs, not by check — before, a misspelled name on a task said that the right one was missing); enumerate(array(5)) → enumerate() of a 0-d array: it is a scalar and has no rows — use to_list(a) or the value itself; parquet_scan(bytes) → parquet_scan: expected a path, got bytes — for bytes already in memory, parquet_read(bytes)`.

synsema check (engine v0.6.45+) catches the call before it runs: a call to a name that exists nowhere in the file — not a builtin, not defined, not used as a value — is the warning `p.syn:3: warning: len(...) — there is no task, variable or builtin named 'len' in this file; it fails when that line runs — did you mean length(x)? (without(xs, k) with no task without` gets the same warning without the hint). A warning does not change the exit code. The other warnings are listed in CLI.

The LLM words reason/decide/analyze/generate and the other keywords (to, type, match…) cannot name what you bind: task f(reason) → 'reason' is a reserved word in Synsema; choose another name for the parameter name. Rename (resolve, why). After a . any word is a member name, so m.decide and tx.to are fine.

Exit codes§

ExitWhen
0success
1parse error, runtime error, or any spawned agent ended in ERROR
2usage error — a missing argument, synsema openapi without serve, or an unknown --flag (rejected, not ignored, on run/test/conform)

Plain run prints a stable one-liner: Runtime error: <file>:<line>:<col>: <msg>. Measuring it in a shell? Redirect (… >/dev/null; echo $?) — don't pipe before echo $?, or you read the pipe's code.

internal error in the engine (a bug, not your program — please report it): … means the engine itself failed; there is nothing to change in your code — report it with the program that triggers it.

Rich diagnostics (--explain)§

synsema run --explain program.syn                # human-readable, on stderr
synsema run --explain --format json program.syn  # structured, for tools/agents

The report adds: source context, call stack, visible variables, the program intent, a classification (data / io / logic / capability / type), whether it's recoverable, and fix suggestions. The exit code is unchanged either way.

Common error shapes§