Skip to main content

Handle errors in agent functions

A function fails when its code throws, when a call it makes fails and isn't caught, or when it goes past a limit. This page covers what the agent or flow sees then, the errors you're likely to meet, and how to keep the conversation going.

What happens when a function fails​

Where it runsWhat happens
Agent toolThe agent reads the error, for example { error: "We couldn't find order A-1001" }, and decides what to tell the user.
Lifecycle or tool hookThe hook's Save result to variable gets null.
Function nodeThe node's output variable gets the error message, or is left empty, and the flow moves on to the next node. There's no separate error path, so check the output in a condition if the flow depends on it.
Run buttonThe panel shows Error with the message, plus the console lines logged before the failure.

When a function fails:

  • Messages it already sent stay sent. A yield sendMessage(…) before the failure has reached the user.
  • Table and API calls it already made stay made. A row inserted before the failure stays inserted.
  • Global variable updates are dropped for agent tools and hooks. See When changes are saved.
  • Console lines are kept, so you can see how far it got in the run panel or in Analysis.

The error is the message only. It doesn't include a stack trace or a line number, so put enough in the message, or in a console.error, to find the cause.

Throw or return?​

Both are fine. Choose by who should act on the problem.

Return an error resultThrow
The functionFinishes, and its global variable updates are savedFails, and its updates are dropped
The agentReads your result and replies from itReads the error message
A Function nodeGets your result in its output variable, for a condition to checkGets the error message
Use it forExpected outcomes the user should hear about: not found, out of stock, invalid inputBugs and states the function can't handle
import { tables } from "@yellow/bot";

export default async function* ({ orderId }) {
if (!/^A-\d{4,}$/.test(orderId)) {
// Expected: tell the agent so it can ask again
return { ok: false, reason: "invalid_order_id", message: "Order numbers look like A-1001." };
}

const [order] = await tables.search("orders", { "orders.order_id": orderId }, { limit: 1 });
if (!order) {
return { ok: false, reason: "not_found" };
}
if (typeof order.total !== "number") {
// Unexpected: the data is broken, so fail loudly
throw new Error(`Order ${orderId} has no total`);
}
return { ok: true, status: order.status, total: order.total };
}

A result with a clear ok (or found) field and a reason is easy for an agent to explain and for a flow condition to check.

Catch failing calls​

apis.execute throws when the API answers with an error status. tables calls throw when a filter or value is refused. Catch them where the function can still give a useful answer.

import { apis } from "@yellow/bot";

export default async function* ({ orderId }) {
try {
const response = await apis.execute("getOrderStatus", { orderId });
return { ok: true, status: response.body.status };
} catch (error) {
console.error("Order lookup failed", error); // records the status and body
if (String(error.message).includes("HTTP 404")) {
return { ok: false, reason: "not_found" };
}
return { ok: false, reason: "unavailable", message: "Order lookup is down. Try again in a few minutes." };
}
}

The message of a failed API call has the form api 'getOrderStatus' returned HTTP 404: <start of the response body>, so you can check the status in it.

Errors you may see​

ErrorCauseFix
on runtime 2026-10-19 the default export must be an async generator: export default async function* …A plain async function on runtime 2026.10.19Add the *: export default async function* (…)
your code must export a function as its defaultNo export default, or a named export onlyexport default async function* (inputs, context) { … }
this runtime has no modules to import except @yellow/bot …An import such as "axios" or "node:crypto"Use the modules a function can import
Function 'X' was called with invalid inputs: missing required input 'orderId'The caller didn't pass a required input, passed an undeclared one, or the wrong typeCheck the declared inputs, the node's mapping, or the agent's instructions
table must be a Studio reference starting with '$bot_table_' …A table name typed by handPick the table from the editor's suggestions. See What the editor stores.
column must be a Studio reference starting with '$bot_table_column_' …A column name typed by handPick the column from the editor's suggestions. See Get suggestions.
column '…' belongs to table '…', not '…'A column from a different table than the one you're searching or updatingPick the column under the right table
api must be a Studio reference starting with '$bot_api_' …An API name typed by handPick the API from the suggestions
api 'X' returned HTTP 500: …The API answered with an error statusCatch it, as above
unknown API: XThe API was deleted, or the code was copied from another agentPick it again from the suggestions
botTables.search limit reached: at most 5 botTables.search calls per runMore than 5 searches in one run. The same message names botTables.insert, botTables.update or botApis.execute.Filter in the search instead of searching in a loop
run-node/database: at most 2 host call(s) may be in flight at once …Three or more calls started together with Promise.allRun them two at a time
interrupted: wall/CPU budget exceeded at …Past 60 seconds in total, usually waiting on slow APIsMake fewer or faster calls
interrupted: CPU-time budget exceeded at …Past 2 seconds of your own code runningDo less work in the function, or on less data
interrupted: sync-slice CPU budget exceeded …Half a second of code without an awaitShorten the loop or the data it works on
out of memory: heap limit exceeded …Past 32 MB, often from a large API or search responseAsk for fewer rows or columns, or a smaller response
result too large: … bytes (limit 10485760)A return value over 10 MBReturn only what the caller needs
a function may yield at most 10 times in one runAn 11th yieldCombine messages, for example into one sendCards
haltTurn() halts the agent's turn: return it, don't yield ityield haltTurn(…), yield endCall() or yield transferCall(…)return the action
the function threw a objectthrow { … } instead of an Errorthrow new Error("…") so the message is readable. An Error without a message reaches the caller as an empty message.
BigInt are forbidden in JSON.stringifyA BigInt in the return valueReturn it as text: String(value)

Edge cases to handle​

CaseWhat you getHandle it with
A search finds nothing[]const [row] = await tables.search(…), then if (!row) { … }
An optional input wasn't passedundefinedA default: const { limit = 5 } = inputs
A global variable has no valueundefinedvariables.global.get and a fallback: tier ?? "standard"
An update matches no rowstrue, with nothing changedSearch first if you need to know the row exists
An insert is rejectedAn error, or { success: false, message }try / catch, and check result.success
A JSON columnText, not an objectJSON.parse it. See Work with tables.
A non-JSON API responseresponse.body is textCheck typeof response.body === "string" before reading fields
A chat, not a callcontext.callSid is ""Branch on it before endCall or transferCall
A test runcontext.invokedBy is "dev-run" and sessionId is ""Don't rely on a session in code you test with Run