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 runs | What happens |
|---|---|
| Agent tool | The agent reads the error, for example { error: "We couldn't find order A-1001" }, and decides what to tell the user. |
| Lifecycle or tool hook | The hook's Save result to variable gets null. |
| Function node | The 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 button | The 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 result | Throw | |
|---|---|---|
| The function | Finishes, and its global variable updates are saved | Fails, and its updates are dropped |
| The agent | Reads your result and replies from it | Reads the error message |
| A Function node | Gets your result in its output variable, for a condition to check | Gets the error message |
| Use it for | Expected outcomes the user should hear about: not found, out of stock, invalid input | Bugs 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
| Error | Cause | Fix |
|---|---|---|
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.19 | Add the *: export default async function* (…) |
your code must export a function as its default | No export default, or a named export only | export 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 type | Check 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 hand | Pick 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 hand | Pick 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 updating | Pick the column under the right table |
api must be a Studio reference starting with '$bot_api_' … | An API name typed by hand | Pick the API from the suggestions |
api 'X' returned HTTP 500: … | The API answered with an error status | Catch it, as above |
unknown API: X | The API was deleted, or the code was copied from another agent | Pick it again from the suggestions |
botTables.search limit reached: at most 5 botTables.search calls per run | More 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.all | Run them two at a time |
interrupted: wall/CPU budget exceeded at … | Past 60 seconds in total, usually waiting on slow APIs | Make fewer or faster calls |
interrupted: CPU-time budget exceeded at … | Past 2 seconds of your own code running | Do less work in the function, or on less data |
interrupted: sync-slice CPU budget exceeded … | Half a second of code without an await | Shorten the loop or the data it works on |
out of memory: heap limit exceeded … | Past 32 MB, often from a large API or search response | Ask for fewer rows or columns, or a smaller response |
result too large: … bytes (limit 10485760) | A return value over 10 MB | Return only what the caller needs |
a function may yield at most 10 times in one run | An 11th yield | Combine messages, for example into one sendCards |
haltTurn() halts the agent's turn: return it, don't yield it | yield haltTurn(…), yield endCall() or yield transferCall(…) | return the action |
the function threw a object | throw { … } instead of an Error | throw new Error("…") so the message is readable. An Error without a message reaches the caller as an empty message. |
BigInt are forbidden in JSON.stringify | A BigInt in the return value | Return it as text: String(value) |
Edge cases to handle
| Case | What you get | Handle it with |
|---|---|---|
| A search finds nothing | [] | const [row] = await tables.search(…), then if (!row) { … } |
| An optional input wasn't passed | undefined | A default: const { limit = 5 } = inputs |
| A global variable has no value | undefined | variables.global.get and a fallback: tier ?? "standard" |
| An update matches no rows | true, with nothing changed | Search first if you need to know the row exists |
| An insert is rejected | An error, or { success: false, message } | try / catch, and check result.success |
| A JSON column | Text, not an object | JSON.parse it. See Work with tables. |
| A non-JSON API response | response.body is text | Check typeof response.body === "string" before reading fields |
| A chat, not a call | context.callSid is "" | Branch on it before endCall or transferCall |
| A test run | context.invokedBy is "dev-run" and sessionId is "" | Don't rely on a session in code you test with Run |
Related
- Test and debug: the Run button, tests and logs
- Examples: complete functions with error handling