@yaebal/again

auto-retry on 429 / flood-wait and transient 5xx errors.

install

terminal
pnpm add @yaebal/again

usage

call autoRetry(bot.api) once after constructing the bot. it installs an error hook on the API layer — every failed call is inspected and, if retryable, waited on and re-issued automatically. no middleware registration needed.

bot.ts
import { Bot } from "@yaebal/core";
import { autoRetry } from "@yaebal/again";

const bot = new Bot(process.env.BOT_TOKEN!);

// attach the retry handler to the bot's API layer
autoRetry(bot.api);

// all API calls made through bot.api will now be retried automatically
bot.on("message:text", (ctx) => ctx.reply("hello!"));

bot.start();

options

bot.ts
autoRetry(bot.api, {
  maxRetries: 5,      // retry up to 5 times (default: 3)
  maxDelayMs: 10_000, // cap each wait at 10 s (default: 30 000)
  retryOnInternal: false, // skip 5xx, only handle 429 (default: true)
});

api

exportsignaturedescription
autoRetry(api: Api, options?: AutoRetryOptions) => voidinstalls the retry hook on bot.api
decideRetry(error: unknown, attempt: number, options?: AutoRetryOptions) => ErrorAction | undefinedpure retry-policy function — exported for unit testing
AutoRetryOptionsinterfaceoptions bag passed to both functions

AutoRetryOptions

fieldtypedefaultdescription
maxRetriesnumber3max retries after the first attempt
maxDelayMsnumber30000cap on a single wait in milliseconds
retryOnInternalbooleantruealso retry transient 5xx server errors

retry logic

decideRetry inspects errors in this order:

  • attempt exceeds maxRetries → no retry
  • not a TelegramError → no retry
  • code 429 → reads retry after N from the message; falls back to exponential backoff (2^attempt seconds); capped at maxDelayMs
  • code ≥ 500 and retryOnInternal is true → exponential backoff; capped at maxDelayMs
  • anything else (4xx client errors, unknown errors) → no retry
policy.ts
import { decideRetry, type AutoRetryOptions } from "@yaebal/again";
import { TelegramError } from "@yaebal/core";

const opts: AutoRetryOptions = { maxRetries: 3, maxDelayMs: 30_000 };
const action = decideRetry(new TelegramError("sendMessage", 429, "retry after 7"), 1, opts);
// => { retry: true, delayMs: 7000 }
4xx errors are never retried. a 400 Bad Request means the call itself is wrong — retrying it would loop forever. only 429 and 5xx are candidates.

decideRetry is a pure function with no I/O — it is exported specifically so you can unit-test a custom policy without mocking network calls.