troubleshooting
symptom-driven fixes for the telegram failures users hit most often in real bots.
bot does not start
| symptom | likely cause | fix |
|---|---|---|
401 unauthorized | missing, empty, or wrong token | validate BOT_TOKEN before constructing the bot |
404 not found on every method | wrong apiRoot or token copied with whitespace | trim the token and check custom bot api server url |
| process exits immediately | bot.start() was not awaited, or the runtime finished before async work | use top-level await bot.start() in esm |
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.
// 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();| symptom | fix |
|---|---|
409 conflict | stop old containers, local dev processes, and duplicate bot.start() calls |
| webhook is set | call deleteWebhook or switch the app to webhook mode |
| groups only deliver commands | botfather privacy mode is enabled; disable it only if the bot must read all group messages |
chat_member / reactions never arrive | pass explicit allowedUpdates |
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.
const info = await bot.api.call("getWebhookInfo", {});
console.log(info.url, info.last_error_message);| symptom | fix |
|---|---|
| telegram never reaches localhost | use a public https url or a tunnel for local dev |
401 in your logs | the secretToken passed to setWebhook does not match the handler |
405 | telegram must post to the exact route that mounts the webhook callback |
413 | request 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.
bot.callbackQuery(/^confirm:/, async (ctx) => {
await ctx.answerCallbackQuery(); // answer first so the client spinner stops
await ctx.reply("confirmed");
});formatting is broken
| symptom | fix |
|---|---|
literal <b> or markdown appears in chat | use html/md from @yaebal/fmt or entity builders from core, not raw parse_mode strings |
| user input breaks formatting | interpolate into html/md templates so user text is escaped as literal text |
| entities disappear after string concatenation | keep 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.
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 worksmedia.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.
| problem | fix |
|---|---|
ctx.session is not typed | install session() before the handler and keep the returned bot/composer value in the chain |
| scene plugin cannot read session | install session before scenes/onboarding-style stateful plugins |
| plugin works at runtime but not in typescript | make 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
- search the generated bot api reference for the exact method.
- turn the failing behavior into a test with @yaebal/test.
- check production patterns in production.