troubleshooting

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

error text → cause → fix

search this table for the exact wording Telegram (or Node) gave you:

error textcausefix
message is not modifiedediting a message with identical text/markupskip the edit, or diff before calling editText/editMessageText
can't parse entities: …malformed HTML/Markdown passed straight to parse_modebuild the text with html`…`/md`…` from @yaebal/fmt instead of raw markup strings
query is too old and response timeout expiredanswered a callback query more than ~15s after it arrivedcall answerCallbackQuery() first, before any awaits that can be slow
QUERY_ID_INVALIDreused or already-answered callback_query idanswer each callback query exactly once
chat not foundwrong/stale chat_id, or the bot was never in that chatverify the id; a user must have started the bot or share a chat with it first
bot was blocked by the user403 on send — the user blocked the botcatch and drop this recipient; don't retry it in a broadcast
not enough rights to …the bot lacks the specific admin permission for that actioncheck getChatMember for the bot itself before attempting the action
message to delete not founddeleting a message already deleted or older than 48htreat as success — the end state (message gone) is already true

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() rejected during startup (bad token, no network) and nothing caught ituse top-level await bot.start() in esm so the startup error surfaces with a stack trace
env.ts
import { createBot } from "yaebal";

const token = process.env.BOT_TOKEN;

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

const bot = createBot(token);

ERR_MODULE_NOT_FOUND / esm import errors

yaebal is "type": "module". Node's ESM resolver does not add extensions for you — a local import needs the .js specifier that the compiled output will actually have, even while editing the .ts source.

users.ts
export const loadUser = async (id: number) => ({ id });
esm.ts
// @ts-expect-error — ERR_MODULE_NOT_FOUND at runtime: node's esm resolver
// does not add extensions for you, so this fails to resolve even though
// users.ts sits right next to this file.
import { loadUser } from "./users";

// ✅ write .js even though the source file is users.ts — nodenext rewrites
// nothing at compile time; this is what actually ends up on disk.
import { loadUser as loadUserFixed } from "./users.js";

await loadUserFixed(1);

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
import { createBot } from "yaebal";

const bot = createBot(process.env.BOT_TOKEN!);

// 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
import { Bot } from "@yaebal/core";

const token = process.env.BOT_TOKEN!;

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
import { createBot } from "yaebal";
import type { WebhookInfo } from "@yaebal/types";

const bot = createBot(process.env.BOT_TOKEN!);

const info = await bot.api.call<WebhookInfo>("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
import { InlineKeyboard, createBot } from "yaebal";

const bot = createBot(process.env.BOT_TOKEN!);

bot.command("start", (ctx) =>
  ctx.reply("deploy to production?", {
    reply_markup: new InlineKeyboard().text("confirm", "confirm:deploy").build(),
  }),
);

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

bot.start();

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 { createBot, media } from "yaebal";

const bot = createBot(process.env.BOT_TOKEN!);

bot.command("send", async (ctx) => {
  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.

bot feels slow / updates pile up

sequential long polling processes one update at a time by design — a slow handler (a database call, an outbound http request) blocks everything behind it in the same chat and others besides.

symptomfix
updates visibly queue up under load@yaebal/runner — concurrent polling with per-chat ordering preserved
outbound sends get 429'd@yaebal/throttle — buckets outgoing calls under Telegram's own limits
calls fail transiently (5xx, network blips)@yaebal/again — retry with backoff instead of failing the update

still stuck