Keep the key out of output in examples: send it as Authorization: Bearer $SUBSTRIO_KEY
(the app is the host, or X-Substrio-Tenant: <app>). Read the record with
operation function.errors (MCP tool substrio_function_errors,
GET /v1/_function_errors); the same record is carried on a failed
function.invoke and a failed function.execute_inline (POST /v1/_exec).
The record
Every throw is recorded once, where it is thrown, as a typed entry in the app's function error log. The stack is parsed into frames at that moment; nothing later re-parses text.
{
"id": "0000019a1b2c3d4e00000001:acme/thrower",
"function": "acme/thrower",
"revision": 3,
"request_id": "req_41",
"timestamp": 1760000000000,
"name": "Error",
"message": "deep boom",
"frames": [
{"function": "inner", "file": "thrower.js", "line": 1, "column": 31},
{"function": "outer", "file": "thrower.js", "line": 2, "column": 25}
]
}
id:<timestamp as 16 hex digits><sequence as 8 hex digits>:<function id>. It is also the cursor: pass it ascursorto read the entries older than it.function: the function id,<app>/<name>.revision: the function's newest deploy counter when it threw (version.logwith the function path reports it per commit).request_id: the call. The same value tags that call's log lines (function.logsfilters by it).timestamp: Unix milliseconds (UTC).nameandmessageare the thrown error's.framesare innermost first; a stack line the grammar does not fit is kept whole as a frame whosefunctionis the line andlineis 0, so no line is dropped.- Entries are never trimmed: there is no retention cap.
Reading them: function.errors
curl -s "$SUBSTRIO_URL/v1/_function_errors?function=thrower&limit=2" \
-H "Authorization: Bearer $SUBSTRIO_KEY"
Query options (MCP: the same names as tool arguments):
| Option | Meaning |
|---|---|
function |
A function id (acme/thrower) or its bare name. Absent: every function. |
request_id |
Only the entries of this call. |
revision |
Only the entries recorded while this revision (the deploy counter of the function, which version.log reports per commit) was the function's newest: the errors of one deploy. A non-number is a 400. |
since, until |
Unix seconds (UTC), inclusive: until takes its whole second. |
limit |
Page size, a positive integer. Absent: every entry. |
cursor |
A next_cursor this operation answered. Anything else is a 400. |
Response:
{"errors": [ {entry}, ... ], "count": 2, "next_cursor": "0000019a1b2c3d4e00000001:acme/thrower"}
Entries come newest first. next_cursor is null on the last page. A bad
cursor, a zero limit, or since/until past what milliseconds hold answers
INVALID_ARGUMENT (400) with the option named in detail.
On the failed call itself
A failed function.invoke answers the standard error envelope (error, code
CF_EXECUTION_FAILED, status 500, detail the message) plus one more member,
function_error: the entry above without its storage identity.
{"error": "Cloud function execution error", "code": "CF_EXECUTION_FAILED",
"status": 500, "retryable": false, "next_action": "reduce_request_or_increase_limit",
"detail": "deep boom",
"function_error": {
"id": "0000019a1b2c3d4e00000001:acme/thrower", "revision": 3,
"request_id": "req_41", "name": "Error", "message": "deep boom",
"frames": [ ... ]}}
function_error.id is the stored entry's id: pass it as cursor to
function.errors to start at that entry, or read it back with request_id.
function.execute_inline records the same entry under the function id
<app>/_exec, with a fresh request_id, and answers it as function_error
beside error (status 500). Rejections that settle a call after a fetch or
timer carry it too.
A failed scheduled or manual run names its entry in the schedule's history as
error_id (schedules.md).
Refusals the runtime throws
A host call that refuses (a db, chain, kv, ledger, files, cmd, queue or fetch call) throws an Error
whose name is its code from the error table, so a script branches on e.name. The realm's own TypeError
is kept for one fault only: an argument of a type the call does not take (instanceof TypeError holds). A wait
that reaches the call's deadline turns whatever the call would throw into SubstrioTimeoutError, and the call
answers 504 CF_WALL_TIMEOUT. Web APIs (fetch's own argument checks, crypto, streams, formData) throw what
their standards say. The same codes over HTTP: api/errors.md.
e.name |
Value | Retryable | next_action | When |
|---|---|---|---|---|
TypeError |
no | an argument of a type the call does not take | ||
INVALID_ARGUMENT |
0x0102 | no | correct_request |
a value out of range, an unknown or empty name, an unknown $operator, a text the call refuses (a kv key with a character above U+00FF, a command with an unclosed quote or a NUL byte, a file path with an empty or .. segment) |
UNSUPPORTED_OPERATION |
0xF009 | no | choose_supported_operation |
a form the call does not run: a kv or graph write or a substrio service inside db.transaction, a transaction callback that returns a promise, a write after a join or a group, a regex flag other than i, a paged find with skip |
TYPE_MISMATCH |
0x8001 | no | correct_request |
stored data of another type than the call takes: $inc on a field that holds no number, db.kv on a name a collection's tree holds, an array for a unique member |
KEY_NOT_FOUND |
0x4000 | no | refresh_resource |
a live row or db.<name>[id] = doc whose document is no longer stored |
KEY_TOO_LARGE |
0x4001 | no | inspect_detail |
a kv key or a file path past 65,535 bytes |
VALUE_TOO_LARGE |
0x4002 | no | inspect_detail |
a kv value past the native value bound |
COLLECTION_NOT_FOUND |
0xC004 | no | refresh_resource |
a collection that was dropped or cannot be opened |
NO_CALL_CONTEXT |
0xC036 | no | correct_request |
a db, collection, kv, graph, ledger or files handle used after the call that made it |
LEDGER_UNAVAILABLE |
0xC037 | no | inspect_detail |
db.ledger's store could not be opened |
FETCH_FAILED |
0xF00E | no | inspect_detail |
fetch() rejects: the upstream could not be reached or broke off (the message is the client's) |
INVALID_QUERY_SYNTAX |
0xB005 | no | correct_request |
a regex pattern that does not compile |
MISSING_INDEX |
0x4005 | no | inspect_detail |
a search that needs a search configuration the collection does not have |
INVALID_BOUNDS |
0x7002 | no | inspect_detail |
withinBox with no corners or corners of two lengths, withinSphere with an empty center |
INVALID_RADIUS |
0x7003 | no | inspect_detail |
withinSphere with a radius out of range |
SCHEMA_REFUSED |
0x8004 | no | correct_request |
the collection's declared schema refuses the value |
ID_RESERVED |
0x400A | no | correct_request |
an insert or create body that names _id (use put) |
INVALID_ID |
0x400B | no | correct_request |
an id that names no document's form |
MISSING_FIELD |
0xF008 | no | correct_request |
a required member is absent (a put with no id) |
SECRET_FIELD |
0x8006 | no | correct_request |
a filter, sort or projection names a secret field |
ROW_STALE |
0x4012 | no | inspect_detail |
a live-row write after another writer changed the document; the row now holds the stored document (code 0x4012) |
ENGINE_GATE_HELD |
0x4011 | no | inspect_detail |
a write to an engine-only SQL table from inside a query that reads the engine (code 0x4011) |
TIMEOUT |
0x3005 | yes | retry |
a write waited for a script transaction past the call's time and did not start |
STORAGE_IO |
0x1000 | no | inspect_detail |
a tree that could not be made, a write the store refused |
SQL_FAILED |
0xB006 | no | correct_request |
the engine failed to run the chain |
INTERNAL |
0x0106 | no | inspect_detail |
a refusal with no cause of its own (a fault on the Substrio side) |
UniqueError |
no | a write a unique field refuses; code is 'unique', with collection, field and value |
||
RangeError |
no | kv.getInt of a stored integer a number cannot hold, an $inc past int64 or the double range |
||
NotFoundError |
no | firstOrFail() found nothing |
Not covered
function.eval runs Lua, not JavaScript; its failures are its own error
bodies with no frames.