Referencia
Errores y exit codes
Tocá Run para ver un error capturado; editalo (quitá el try/recover) para ver un Runtime error: … sin capturar. Los tests de abajo fijan los mensajes de los errores que más probablemente te cruces.
-- 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)Lanzar y capturar§
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 y stop no son errores — pasan por try/recover. (fail(...) arma una respuesta HTTP, no lanza.) Testeá que algo lanza con assert_error(task). La forma sentencia raise "msg" / raise err (sin paréntesis) también funciona, y un raise solo es error de parseo con el fix en el mensaje. ⚠️ En el v0.5.1 publicado y anteriores, raise "msg" sin paréntesis eran dos expresiones inertes — no-op silencioso; en esos binarios siempre raise("msg").
Un builtin que puede fallar con entrada no confiable suele tener una forma total que devuelve un fallback en vez de lanzar: json_decode(text, nothing), int(text, fallback = nothing), number(text, nothing).
Errores que nombran el arreglo§
Un mensaje dice qué escribir, no solo qué salió mal. Los reflejos de otros lenguajes se reconocen al cargar (así el programa nunca arranca a medias mal) y se contestan con la forma Synsema:
| Escribiste | El error dice |
|---|---|
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] | el template con backticks / a lambda is (y) => y / no comprehensions: apply(…) … where(…) / no slice syntax: slice(xs, start, end?) |
// nota (un comentario estilo C) | `comments in Synsema start with -- (//` es división entera) |
5--1 | ` -- right after a value: … write 5 - -1, or 5 -- comment ` |
Y en runtime: 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`.
Desde el motor 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 también lo avisa); in_units(90, "minutes") → in_units() takes a duration, got number — make one first: in_units(duration(seconds = 90), "minutes"); un argumento con nombre que no existe → unknown parameter 'limt' — did you mean 'limit'?; the parameters are: limit, offset (tasks, lambdas y builtins; se detecta cuando corre la llamada, no en check — antes, un nombre mal escrito en una task decía que faltaba el correcto); 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 (motor v0.6.45+) atrapa la llamada antes de que corra: una llamada a un nombre que no existe en ningún lado del archivo — no es builtin, no está definido, no se usa como valor — es el aviso `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) sin un task without` recibe el mismo aviso sin la pista). Un aviso no cambia el código de salida. Los otros avisos están en CLI.
Las palabras LLM reason/decide/analyze/generate y las demás keywords (to, type, match…) no pueden nombrar lo que bindeás: task f(reason) → 'reason' is a reserved word in Synsema; choose another name for the parameter name. Renombrá (resolve, why). Después de un . cualquier palabra es un nombre de miembro, así que m.decide y tx.to andan.
Exit codes§
| Exit | Cuándo |
|---|---|
0 | éxito |
1 | error de parseo, error de runtime, o algún agente spawneado terminó en ERROR |
2 | error de uso — un argumento faltante, synsema openapi sin serve, o un --flag desconocido (rechazado, no ignorado, en run/test/conform) |
El run plano imprime una línea estable: Runtime error: <file>:<line>:<col>: <msg>. ¿Lo medís en una shell? Redirigí (… >/dev/null; echo $?) — no pipees antes de echo $?, o leés el code del pipe.
internal error in the engine (a bug, not your program — please report it): … significa que falló el engine mismo; no hay nada que cambiar en tu código — reportalo con el programa que lo dispara.
Diagnósticos ricos (--explain)§
synsema run --explain program.syn # legible, en stderr
synsema run --explain --format json program.syn # estructurado, para tools/agentes
El reporte agrega: contexto de fuente, call stack, variables visibles, el intent del programa, una clasificación (data / io / logic / capability / type), si es recuperable, y sugerencias de fix. El exit code no cambia en ningún caso.
Formas comunes de error§
- Capacidad:
Capability not granted: net("…")— agregá elrequirecorrespondiente (o ampliá el scope). Si dice declared but above the host ceiling, elrequireya está: solo el host (--sandbox/--cap-set/el techo del embebedor) puede permitirlo.Capability not granted: file_read("…")desde unrendersignifica que no se declaró la lectura de un template en disco — agregárequire file.read("…")(o empaquetá el template consynsema build). - Perfil pure:
<name>: not available in the pure profile — this run has no filesystem(e igual sin procesos hijo / sockets / drivers de base de datos / threads de scheduler) — el builtin quedó amurallado por--profile pure; quitá el flag para correrlo en nativo. Ver Sandboxing. run_program:above parent ceiling/above parent profileen el audit del hijo (un hijo pidió más de lo que presta el padre — recortado, no fatal);run_program: timed out after Ns(timed_out: true);run_program: max depth N exceeded;run_program: env "X" is a secret — reveal() it explicitly.synsema build:bundle corrupt (sha256 mismatch)(un binario construido manipulado se niega a correr);"<path>" is part of the bundle (read-only)(una escritura a un asset empaquetado);this is a built program (synsema build); rebuild it…(--engine updatesobre un binario construido); en tiempo de build,ausewith a dynamic path cannot be bundledy'…' escapes the bundle root.- Etiquetas (v0.6.24+, bajo
--labels/serve --attested— ver Etiquetas):label_violation: <camino> is private to <principal>, the sink accepts (public)— un valor privado llegó a un sumidero público; haceledeclassifycon un motivo, o dejalo adentro.label_violation: print called under private control flow— la rama es el secreto, así que sacá la salida de ahí.private: labels are off; run with --labels or serve --attested— el programa necesita la segunda pared y no está encendida. Dos cosas para saber: un error causado por un dato privado no es atrapable (try/recoverlo re-propaga — usá las variantes totales), y una violación de etiquetas termina toda la corrida desynsema test --labelsen vez de fallar un bloque. - Attestation (v0.6.24+ — ver Attestation):
attestation_verify: opts.now is required (unix seconds);attestation_document: this server is not attested (start it withsynsema serve --attested);reveal: secret(attestation_key) is sealed; y, sin plataforma,run --attestfalla cerrado — jamás se publica un artefacto ante una duda. - Llamadas:
task 'f' is missing argument 'b' — pass it, or give the parameter a default;task 'f' takes 1 argument, got 2;upper() takes at most 1 argument, got 2;upper() does not accept named arguments (got s = …); pass it by position. - Tipo: una operación recibió el tipo equivocado, y nada se convierte a tus espaldas —
Cannot add text and nothing — convert it on purpose: text(x), or interpolate it;fmt: no value for {a};int(x, 16): the second argument is the fallback value, not the base — for base 16 write int(x, base = 16)…(también para2.0);int(1.5): not a whole number;number("1234…") would lose digits … Use int(x);bytes(33)→ usáint_to_bytes(33, size)obytes([33]);cannot mix decimal and float;length: a 0-d array has no length (it is a scalar) — use size(a);json_encode: the value["f"] is a task, not data;as_secret(123)→ "expects text or bytes". - Datos: una key de map faltante (
Map has no key 'b'— oget(m, "b", default)), un índice fuera de rango, al leer o al escribir (Index 10 out of bounds (list length 1)), un índice que no es entero (index must be an integer, got 1.5), JSON inválido enjson_decode. - Archivos (motor v0.6.45+):
read_file,read_file_bytesyedit_filedicen por qué no pueden leer.File not found: <path>es solo para lo que no existe; un archivo que existe daCannot read file <path>: <por qué>—it is a directory (use list_dir),it is not valid UTF-8 text (use read_file_bytes),it is a broken link (it points to something that does not exist),permission denied, o en Windowsin use by another process (it is open without sharing; retry later)(el que vale la pena reintentar). Antes, todos los casos eranFile not found.
- Errores de carga (motor v0.6.46+, el programa no arranca): `
unknown capabilitynettinrequire— did you meanrequire net?; a name that does not exist grants nothing. Known: net, file, …— corregí el nombre (también uno con punto,require file.raed(...), y dentro de una task o un agente).'declassify' is read as a value here (an alias, a list or map entry, an argument): it can only be called by its name,declassify(…), so that every call is in the review (synsema code check)— llamala comodeclassify(x, "por qué"); lo mismo paraprivate, y una clave de mapa sin comillas con uno de esos nombres ({private: 1}) va entre comillas,{"private": 1}.unknown type 'int' for parameter 'a': the parameter types are text, number, bool, list or mapythe default of 'a: number' is a text` — usá uno de los cinco tipos, y un default de ese tipo. - Etiquetas, v0.6.46 (ver Etiquetas): `
label_violation: declassify is not granted for what this value is private to (clientes). With a ceiling the host names what a program may publish (--cap-set declassify=<principal>), and inside asandboxonly asandbox underthat delegatesdeclassifyallows it— el host agregadeclassify=<principal>a su techo (o lo delega consandbox under {"declassify": …}); como toda violación de etiquetas no se puede atrapar, y el mensaje nombra el conjunto declarado, no la etiqueta real del valor.label_violation: reason would send data private to (customers) to the model; the host allows that per principal and provider in its ceiling (--cap-set llm=<principal>@<provider host>,@localfor a model in the process,@embedderin wasm) and the program declaresrequire llm("<principal>"). Otherwise declassify(…) the part the model may see— lo mismo paraanalyze/decide/generate/llm_step/llm_stream, y conjudge=/require judge(...)para un bloquejudge.label_violation: read_line prompt under private control flow (pc = [a]); …` — un prompt privado, o un prompt dentro de una rama decidida por un valor privado: sacalo de la rama, o desclasificalo (hoy el mismo mensaje cubre un prompt privado fuera de una rama). - Llamadas a tools con parámetros tipados (motor v0.6.46+, ver Tool calling): `
call_tool(add): 'a' must be number, got text — a tool call fromllm_stepbrings typed arguments only for the parameters the catalog types (tool_spec(task, describe)takes them from the signature)— armá el catálogo contool_spec, o convertí dentro de la task;call_tool(add): missing 'a' (number)— pasalo, o dale un default al parámetro.call/call_tool` chequean los tipos de forma estricta y no convierten nada; una llamada normal no los chequea. - Módulos de confianza (motor v0.6.46+, ver Módulos § de confianza): `
no trusted module named 'nope': the host loads it (synsema run --trusted nope=./file.syn, ortrusted: { nope: source }in wasm); a program cannot load one itself— el host pasa--trusted; una task, una lambda o un builtin pasados a una tool de confianza, un argumento con nombre, unrequirede nivel superior en el módulo de confianza,--trustedbajoserve, y la familiaa trusted tool cannot … yet` nombran cada uno qué hacer en su lugar. - Secretos, v0.6.46 (ver Secretos): `
json_encode: the value["auth"]["key"] is secret(K), and a secret is never written as text — pass the value to the request itself (in a header; in the body or the URL once it is bound:require secret("K") for net("host")) and the engine writes it at the socket, or leave it out of what you encode(tambiénjsonl_encode,json_for_script, ycsv_encodeconrow N, column "c") — antes el secreto salía como"[redacted]";a secret inside a map cannot go into a text hole (secret(K) would travel as its placeholder): take it out on its own (creds.k) or use bearer(...)/basic(...). Cuando el host ata un secreto (--cap-set secret=K@host):secret("K") not permitted: the host binds K to api.example.com; writerequire secret("K") for net("api.example.com")`. - Recibos sellados (motor v0.6.46+, ver Observabilidad § Recibos sellados):
--auditor: the auditor key must be an X25519 public key of 32 bytes, got Ny… cannot seal (a low-order X25519 point…)— pasá lapublicdesynsema receipt keygen;--receipt needs an auditor…— agregá--auditoroSYNSEMA_AUDITOR_KEY;SYNSEMA_RECEIPT_SEALED_BYTES must be between 4096 and 16777216, got N; `serve --auditor: routeGET /xis a stream route: its body leaves while it runs…— servila desde un server sin auditor;--replay: f is not an opened receipt…— pasale la salida dereceipt open;replay: input #N differs from the recorded run — …— la entrada cambió desde la corrida grabada;the receipt's signature does not verify (…)(el recibo fue alterado),the chain does not verify: the entries are not the ones the run signed(faltan entradas o se reordenaron),the envelope does not open with this key (…)(clave equivocada, o se tocó el header),<file> has 2 sealed receipts (lines 1, 2): pick one with --line N`. - Hooks del host en wasm (motor v0.6.46+): un
kvdel host que tira o responde otra cosa es un error del builtin (state_get: host kv.get failed: …,kv.get must return a string or null, got a number,kv.list must return a list of strings, got …), no una clave ausente — la tabla completa y cómo migrar: WebAssembly § Qué tiene que contestar un hook. - Red, v0.6.46 (ver Cliente HTTP y Capacidades): `
http_get:127.0.0.1.nip.ioresolved to 127.0.0.1, a private address; anetgrant with a wildcard reaches only public addresses. Name it:require net("127.0.0.1.nip.io:47290"), its address or its CIDR (the scope can come from the configuration:require net(env("…_URL")))— los builtins HTTP lo devuelven enerror(constatus: 0) como cualquier falla de transporte;tcp_connect/ws_connectlo lanzan;proxy tole contesta al cliente502 proxy error: upstream not allowed` y deja el motivo en el log. Escribí la línea que nombra. - Secretos ligados (motor v0.6.46+, ver Secretos): `
http_get: the URL is secret(K), and an unbound secret only goes in a header. If the API takes it in the URL, bind it to the host it goes to:require secret("K") for net("api.example.com")— escribí esa línea;secret(K) is bound to [api.example.com]; this request goes to other.example.com — it never goes to another host, not even in a header— mandalo sólo a un host de su ligadura;secret(K) only travels over https; this request to api.example.com is http://— usá https (http y ws sólo a127.0.0.1,localhost,::1);the URL is secret(K) and its host is not written out before the secret; write the scheme and host first (https://host/…{key}): the host of a request never comes from a secret;require secret("K") for net(...): binding a secret to every host is not binding it: name the host it goes to(error de carga, también para*.com`).