ptgb
A complete Telegram Bot API client for Dart

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.