Skip to main content

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}`)
warning

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 err binds the error message to err.
  • finally always 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.

Next