Error Handling
ptgb does not retry or throttle requests for you. Every failed Bot
API call throws a TelegramApiException.
TelegramApiException
class TelegramApiException implements Exception {
final int errorCode; // e.g. 400, 403, 429
final String description; // human-readable explanation from Telegram
final Json? parameters; // extra machine-readable details, e.g. retry_after
}
Thrown whenever the Telegram Bot API responds with "ok": false. Inspect
errorCode and description (and parameters for special cases like
rate limiting) to decide how to react.
Basic pattern
try {
await bot.sendMessage(chatId, 'hi');
} on TelegramApiException catch (e) {
if (e.errorCode == 429) {
final retryAfter = e.parameters?['retry_after'] as int?;
print('Rate limited, retry after $retryAfter seconds');
}
}
Why this matters
Wrap your update-handling logic in try/catch. Without it, a single bad
call — a user who blocked the bot, a rate limit, an invalid chat_id —
will crash your whole process:
await for (final update in bot.poll()) {
try {
await handleUpdate(update);
} on TelegramApiException catch (e) {
print('Failed to handle update: ${e.errorCode} ${e.description}');
}
}
Common error codes to plan for
| Code | Typical cause | Suggested handling |
|---|---|---|
400 |
Malformed request (bad chat_id, invalid parameter combination) |
Fix the call; don’t retry as-is |
403 |
Bot was blocked by the user, kicked from the chat, or lacks permission | Stop messaging that chat/user |
404 |
Wrong token / method disabled | Check configuration, don’t retry |
429 |
Rate limited | Back off for parameters['retry_after'] seconds, then retry |
Non-JSON responses
If an intermediary between your bot and Telegram (a proxy, load balancer, etc.) returns something that isn't valid JSON — an HTML error page during a 502/503, for example — ptgb still throws a TelegramApiException rather than letting a raw FormatException escape. The same try/catch you already have around TelegramApiException covers this case too; errorCode is set from the HTTP status code.
poll() retries transient network errors automatically
Timeouts, socket errors, and similar transport-level failures no longer kill the poll() stream. poll() catches them internally, waits with exponential backoff (starting at initialBackoff, doubling up to maxBackoff, resetting after a successful call), and keeps polling:
await for (final update in bot.poll(
onError: (error, stackTrace) => print('poll transient error: $error'),
initialBackoff: const Duration(seconds: 1),
maxBackoff: const Duration(seconds: 30),
)) {
// ...
}
Pass onError to observe these failures (for logging, metrics, etc.) — leave it out and they're retried silently. This only covers transport-level failures; a TelegramApiException thrown by your own await bot.someMethod(...) calls inside the loop still propagates normally and needs its own try/catch, as above. serveWebhook has an equivalent onError for exceptions thrown while parsing a request or inside your onUpdate callback — the request still gets an HTTP 200 either way, since Telegram doesn't inspect the response body.
Full retry/backoff example
See example/15_error_handling_and_retries.dart for a complete pattern
that distinguishes rate limits, blocked users, and other failures, and
example/11_god_mode_bot.dart for a simpler catch-and-log version used
across many features at once.