Error Handling
External calls fail: APIs time out, links can't be created, payments hiccup. Production bots handle these gracefully instead of dying silently. Botgami gives you on error { } for calls, try/catch/finally for blocks, and fail { } for subflows.
on error on HTTP and await calls
Attach an on error { } block to an http.* call or an await of a subflow. It runs if the call fails:
let { temp, description } = await openweather.current_weather(
city: flow.city,
api_key: global.openweather_api_key
) on error {
send.text("⚠️ Couldn't reach the weather service. Try again shortly.")
stop
}
send.text(`${flow.city}: ${temp}°C, ${description}`)
res = http.get(url: "https://api.example.com/data") on error {
send.text("Service unavailable. Please retry.")
stop
}
send.text(`Status: ${res.status_code}`)
Never call http.* on a mission-critical path without an on error handler. A bare call that fails will abort the flow with no message to the user. For unreliable endpoints, use the http.retry recipe.
A fallback inside on error
on error is also handy to provide a default value so the flow can continue:
let { result } = await format.money(amount: flow.price, symbol: "$") on error {
set { flow.pretty = `$${flow.price}` } // fallback formatting
}
set { flow.pretty = result }
send.text(`Total: ${flow.pretty}`)
try / catch / finally
For a block of steps (rather than a single call), wrap it:
try {
lk = chat.invite_link(chat_id: shared.group_chat_id, name: "Invite", creates_join_request: true)
send.text(`🔗 Your link: ${lk}`)
} catch err {
send.text(`❌ Couldn't create a link: ${err}`)
} finally {
set { shared.invite_attempts += 1 }
}
catch errbinds the error message toerr.finallyalways runs, error or not.
fail { } in subflows
A subflow reports failure with fail { }. The caller catches it with on error:
subflow withdraw(amount: Number) -> { ok: Boolean } {
when user.balance < amount {
fail { reason: "Insufficient balance" }
}
set { user.balance -= amount }
return { ok: true }
}
flow cash_out on command "/cashout" {
let { ok } = await withdraw(amount: 100) on error {
send.text("❌ Not enough balance.")
stop
}
send.text("✅ Withdrawal complete.")
}
Graceful empty states and timeouts
Two patterns you should apply everywhere:
Empty state — always handle the empty list:
when util.len(user.tasks) == 0 {
send.text("📭 You have no tasks yet. Add one with /add!")
} else {
foreach t in user.tasks { send.text(`• ${t.title}`) }
}
Ask timeout — every blocking ask.* handles "timeout":
branch ask.text("Your answer?", timeout: "120s") {
"timeout" { send.text("No response — say /start to retry.") stop }
default { send.text(`Got it: ${answer}`) }
}
The http.retry recipe
For flaky upstreams, retry with backoff instead of hand-rolling a loop:
http.retry(url: "https://api.example.com/data", method: "GET", max_attempts: 3, backoff: "2s") {
on success {
send.text("✅ Fetched.")
}
on failed {
send.text("❌ Still failing after retries.")
}
}
See Recipes for the full signature.