troubleshooting

symptom-driven fixes for the telegram failures users hit most often in real bots.

bot does not start

symptomlikely causefix
401 unauthorizedmissing, empty, or wrong tokenvalidate BOT_TOKEN before constructing the bot
404 not found on every methodwrong apiRoot or token copied with whitespacetrim the token and check custom bot api server url
process exits immediatelybot.start() was not awaited, or the runtime finished before async workuse top-level await bot.start() in esm
env.ts
const token = process.env.BOT_TOKEN;

if (!token) {
  throw new Error("BOT_TOKEN is missing");
}

const bot = createBot(token);

polling receives no updates

telegram allows either long polling or webhooks for a token, not both. a common failure is 409 conflict: terminated by other getUpdates request: another process is polling the same token, or a webhook is still registered.

reset-polling.ts
// if polling gets 409 conflict, make sure no webhook is set
await bot.api.call("deleteWebhook", { drop_pending_updates: true });

// then start exactly one polling process
await bot.start();
symptomfix
409 conflictstop old containers, local dev processes, and duplicate bot.start() calls
webhook is setcall deleteWebhook or switch the app to webhook mode
groups only deliver commandsbotfather privacy mode is enabled; disable it only if the bot must read all group messages
chat_member / reactions never arrivepass explicit allowedUpdates
allowed-updates.ts
const bot = new Bot(token, {
  allowedUpdates: [
    "message",
    "callback_query",
    "chat_member",
    "message_reaction",
    "chat_join_request",
  ],
});

webhook does not fire

use getWebhookInfo first. telegram tells you the registered url, pending update count, and the last delivery error.

webhook-info.ts
const info = await bot.api.call("getWebhookInfo", {});
console.log(info.url, info.last_error_message);
symptomfix
telegram never reaches localhostuse a public https url or a tunnel for local dev
401 in your logsthe secretToken passed to setWebhook does not match the handler
405telegram must post to the exact route that mounts the webhook callback
413request body exceeded the built-in 1 mib guard; real updates should be tiny

see webhooks and @yaebal/web.

callback button spinner hangs

telegram clients show a loading spinner until the bot answers the callback query. answer it at the top of the handler, then edit or send messages.

callback.ts
bot.callbackQuery(/^confirm:/, async (ctx) => {
  await ctx.answerCallbackQuery(); // answer first so the client spinner stops
  await ctx.reply("confirmed");
});

formatting is broken

symptomfix
literal <b> or markdown appears in chatuse html/md from @yaebal/fmt or entity builders from core, not raw parse_mode strings
user input breaks formattinginterpolate into html/md templates so user text is escaped as literal text
entities disappear after string concatenationkeep values as yaebal format results until the final ctx.send/ctx.reply

media upload fails

use media.path for local files, media.buffer for in-memory bytes, media.url for public urls, and raw strings for telegram file_ids.

media.ts
import { media } from "@yaebal/core";

await ctx.sendPhoto(media.url("https://example.com/cat.jpg"));
await ctx.sendDocument(media.path("./report.pdf"));
await ctx.sendPhoto("AgACAgIAAx..."); // existing file_id also works
edge runtimes have no filesystem. media.path() needs a runtime file reader and is not available on cloudflare workers. use media.url() or media.buffer() on edge.

session or plugin fields are missing

in yaebal, plugin context fields exist downstream of the .install() call. install plugins before handlers that use them, and encode plugin dependencies in the plugin type when you write your own.

problemfix
ctx.session is not typedinstall session() before the handler and keep the returned bot/composer value in the chain
scene plugin cannot read sessioninstall session before scenes/onboarding-style stateful plugins
plugin works at runtime but not in typescriptmake it a Plugin<In, Out> and return the augmented composer

types look too weak

use createBot() from the yaebal meta package for rich generated runtime contexts. a bare new Bot(token) from @yaebal/core intentionally exposes the small base context unless you provide a custom context factory.

still stuck