Send the key as Authorization: Bearer $SUBSTRIO_KEY and keep it out of output.
Select the app by its host or with X-Substrio-Tenant: <app>. Read errors with
function.errors (MCP tool substrio_function_errors, GET /v1/_function_errors).
A failed function.invoke or function.execute_inline (POST /v1/_exec) also
returns structured error details.
The record
Every throw is recorded once in the app's function error log, where it occurs. The stack is parsed into frames when the entry is recorded.
{
"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. An invalid
cursor, a zero limit, or since/until outside the range representable in
milliseconds returns INVALID_ARGUMENT (400), with the option named in detail.
On the failed call itself
A failed function.invoke returns the standard error envelope (error, code
CF_EXECUTION_FAILED, status 500, detail the message) plus one more member,
function_error. It contains the entry's id, revision, request id, name,
message, and frames.
{"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 read older entries, or find the entry with request_id.
function.execute_inline records the same entry under the function id
<app>/_exec, with a fresh request_id, and returns 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 refused host call (db, chain, KV, ledger, files, cmd, queue, or fetch) throws an Error whose name
is its code from the error table. A script can branch on e.name. The realm's TypeError is reserved for
an argument of a type the call does not accept, and instanceof TypeError holds. If a wait reaches the call's
deadline, the error becomes SubstrioTimeoutError, and the call returns 504 CF_WALL_TIMEOUT. Web APIs,
including fetch argument checks, crypto, streams, and formData, throw errors according to their standards.
See api/errors.md for the HTTP codes.
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, or a document string or binary past 2,147,483,646 bytes |
RECORD_TOO_LARGE |
0x4013 | no | inspect_detail |
a document that would be past 4,294,967,295 bytes once stored |
TOO_MANY_ELEMENTS |
0x4014 | no | correct_request |
an array that would hold more than 4,294,967,295 elements |
FIELD_NAME_TOO_LARGE |
0x8008 | no | correct_request |
a field name past 65,535 bytes |
TOO_DEEP |
0x8009 | no | correct_request |
a value nesting more than 1024 objects and arrays, or an update path of more than 1024 steps |
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. It is not in the current release (Lua comes with the
Redis frontend, SUBSTRIO_BUILD_REDIS): there it answers NOT_IMPLEMENTED
(0x0002, HTTP 501), and substrio.lua.eval throws "substrio.lua.eval: Lua
scripting is not in this build".