ptgb
A complete Telegram Bot API client for Dart

Error Handling & Retries

Part of the Examples. Handling TelegramApiException: rate limits (429), blocked chats (403), and retry/backoff.

Source: example/15_error_handling_and_retries.dart

// ignore_for_file: file_names
// (numbered intentionally for reading/run order -- see README.md)

// ============================================================================
// 15 — ERROR HANDLING AND RETRIES
// ============================================================================
//
// ptgb does not retry failed requests or throttle them for you — every
// failed Bot API call surfaces as a `TelegramApiException` and it's up to
// your bot to decide what to do next. That decision depends on *why* it
// failed:
//
//   - 429 Too Many Requests — you're rate-limited. Telegram tells you
//     exactly how long to wait in `parameters['retry_after']` (seconds).
//     Wait that long, then retry the same call.
//   - 403 Forbidden — the user blocked the bot, left the chat, etc. This
//     will never succeed on retry; stop messaging that chat_id.
//   - Anything else (400 Bad Request, 500, network errors, ...) — usually a
//     bug in your request or a transient server issue. Log it and move on,
//     optionally with a couple of quick retries.
//
// This example wraps `sendMessage` in a small helper that handles all
// three cases, then hammers it with messages to demonstrate hitting a
// rate limit in practice.
//
// HOW TO RUN:
//   1. Create a file named `.env` next to this script containing:
//        TOKEN=123456:ABC-your-token-here
//   2. dart run example/15_error_handling_and_retries.dart
// ============================================================================

import 'dart:developer';

import 'package:ptgb/ptgb.dart';

/// Sends [text] to [chatId], automatically waiting out rate limits and
/// giving up (without throwing) if the chat is unreachable.
///
/// [maxAttempts] bounds how many times we'll retry a rate-limited or
/// transient failure, so a persistently broken call can't loop forever.
Future<void> sendMessageReliably(
  Bot bot,
  int chatId,
  String text, {
  int maxAttempts = 5,
}) async {
  for (var attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      await bot.sendMessage(chatId, text);
      return;
    } on TelegramApiException catch (e) {
      if (e.errorCode == 429) {
        // Telegram tells us exactly how long to back off.
        final retryAfter = e.parameters?['retry_after'] as int? ?? 1;
        log('Rate limited sending to $chatId, waiting ${retryAfter}s '
            '(attempt $attempt/$maxAttempts)...');
        await Future<void>.delayed(Duration(seconds: retryAfter));
        continue;
      }

      if (e.errorCode == 403) {
        // The user blocked the bot / left the chat — retrying won't help.
        // A real bot would mark this chat_id inactive in its own storage.
        log('Chat $chatId is unreachable (403): ${e.description}. Giving up.');
        return;
      }

      // Anything else: log it and back off briefly before retrying, in
      // case it was a transient network/server hiccup.
      log('sendMessage to $chatId failed (${e.errorCode}): ${e.description}');
      if (attempt == maxAttempts) return;
      await Future<void>.delayed(Duration(seconds: attempt));
    }
  }
}

Future<void> main() async {
  final bot = Bot();

  await for (final update in bot.poll()) {
    final chatId = update.chatId;
    if (chatId == null || update.text != '/spam') continue;

    // Fire off a burst of messages fast enough to realistically trigger
    // Telegram's rate limiting, so you can see `sendMessageReliably` back
    // off and recover instead of crashing the loop.
    for (var i = 1; i <= 30; i++) {
      await sendMessageReliably(bot, chatId, 'Message #$i');
    }
    await sendMessageReliably(bot, chatId, 'Done — sent 30 messages.');
  }
}

Run it from the package root:

dart run example/15_error_handling_and_retries.dart