· 5 min read
Telegram Bot API’s Most Common Mistake: Treating Updates Like Commands
A lot of Telegram bots break in the same place: developers assume every incoming update is a clean command and build logic around that assumption. In practice, the Bot API delivers a stream of different update types, and the difference between a reliable bot and a flaky one usually comes down to handling that stream correctly.
If you build Telegram bots long enough, you’ll see the same failure pattern over and over: the bot works in a happy-path demo, but starts behaving unpredictably once real users begin interacting with it.
The root cause is often simple:
The developer treats every incoming update as if it were a command message.
That assumption is wrong.
Telegram’s Bot API does not send a neat sequence of /start, /help, /buy, /confirm and nothing else. It sends updates: messages, edited messages, callback queries, inline queries, chat member changes, poll answers, shipping queries, and more. If your bot logic assumes the only thing that matters is message.text, you will eventually miss important events, process the wrong ones, or crash on null fields.
The mistake in practice
A common first implementation looks something like this:
bot.on('message', (msg) => {
if (msg.text === '/start') {
return startFlow(msg.chat.id);
}
if (msg.text === '/help') {
return sendHelp(msg.chat.id);
}
// everything else
return handleUserInput(msg.text);
});
This works until users do any of the following:
- tap an inline keyboard button
- send a photo instead of text
- edit a previous message
- interact with a callback-based flow
- join or leave a group where the bot is present
- trigger a payment or poll response
At that point, msg.text may be missing, irrelevant, or not the right signal for the action you need.
Why this happens
Telegram uses a multi-modal event model.
An “update” can represent:
- a new message
- an edited message
- a callback query from an inline keyboard
- a chosen inline result
- a channel post
- a reaction or metadata event, depending on API version and client support
Each update type has a different payload shape. The Bot API is intentionally flexible because bots are used in many contexts: private chats, groups, supergroups, channels, and inline mode.
If you flatten all of that into “incoming command text,” you lose the structure that makes Telegram useful.
What to do instead
Think in terms of update routing, not just message parsing.
Your bot should answer three questions for every update:
- What type of update is this?
- Is this update relevant to the current conversation state?
- Which handler should process it?
That changes your design from:
- “If the text is
/start, do X”
into:
- “If this is a callback query in an order flow, do X”
- “If this is a text message in onboarding state, do Y”
- “If this is a photo upload in verification state, do Z”
Handle update types explicitly
A solid bot usually branches by update type first.
Message updates
Use message for user-generated content in chats. But even here, don’t assume text exists.
A message may contain:
textphotodocumentvoicestickerlocationcontactpoll
If your bot accepts only text, verify that message.text exists before using it.
Callback queries
If you use inline keyboards, the user’s action arrives as a callback_query, not a normal message.
This is a frequent source of confusion because the user appears to have “clicked a button,” but the bot receives a separate update with fields like:
callback_query.datacallback_query.messagecallback_query.from
If you ignore callback queries, your buttons will look broken even though Telegram is behaving correctly.
Edited messages
If your bot cares about content changes, handle edited_message separately.
For example, moderation bots, link preview bots, or bots that maintain state based on user content may need to recalculate when a message is edited. If you only process the original message, your state can drift from reality.
Don’t use text commands as your only state machine
Another common mistake is to encode all state in free-form text commands:
/name/email/confirm
That may be fine for a toy bot, but it becomes fragile fast.
Why?
Because users don’t always follow the script.
They:
- send
/helphalfway through a flow - press an old button after state changed
- paste unexpected content
- interact from multiple devices
- retry actions after network delays
A better approach is to keep explicit per-user or per-chat state in your own storage.
For example:
{
"chat_id": 123456,
"state": "awaiting_email",
"flow": "signup"
}
Then your handler can decide whether incoming data belongs to the current flow.
This makes the bot predictable even when Telegram users are not.
A practical routing pattern
A simple structure looks like this:
function handleUpdate(update) {
if (update.message) {
return handleMessage(update.message);
}
if (update.callback_query) {
return handleCallback(update.callback_query);
}
if (update.edited_message) {
return handleEditedMessage(update.edited_message);
}
return logUnhandledUpdate(update);
}
Then inside handleMessage, branch by content and state:
function handleMessage(message) {
const chatId = message.chat.id;
const state = getState(chatId);
if (message.text && message.text.startsWith('/')) {
return handleCommand(message);
}
if (state === 'awaiting_email' && message.text) {
return saveEmail(chatId, message.text);
}
if (state === 'awaiting_photo' && message.photo) {
return savePhoto(chatId, message.photo);
}
return sendFallback(chatId);
}
This is a lot more reliable than checking message.text everywhere and hoping the shape matches.
Common edge cases developers miss
1. Message text is absent
Not every message has text. If you dereference message.text without checking, you’ll eventually get a runtime error.
2. Commands are not the only entry point
A user may enter your flow through a button, deep link, or inline interaction. If your code only listens for /start, you’re missing most of the actual product surface.
3. Group chats behave differently
In groups, bots may receive only certain messages unless privacy mode is configured appropriately. Also, mentions and replies matter more there than in private chats.
4. Old updates can arrive late
If your bot is under load, or your deployment restarts, updates may arrive later than expected. Your handlers should be idempotent where possible.
5. Callback queries need acknowledgement
If you handle a callback_query, remember to answer it. Otherwise the client can keep showing a loading state, which users interpret as a broken button.
The debugging mindset that helps
When a Telegram bot acts strangely, don’t start by inspecting your command parser.
Start by logging the raw update shape.
You want to know:
- which update type arrived
- which fields were present
- what state the user was in
- whether the update was expected for that state
A lot of “Telegram is being weird” bugs are actually “my handler assumed the wrong payload” bugs.
A good rule of thumb
If your bot logic starts with:
if (message.text === ...)
pause and ask:
- What if this is not a text message?
- What if the same interaction comes through a callback query?
- What if the user is in a different state?
- What if the update is edited or delayed?
That mental check prevents a surprising amount of production pain.
Bottom line
The Telegram Bot API is powerful because it models real chat behavior, not just command-line input. The most common mistake is reducing that rich update stream to “text commands only.”
If you want a bot that survives real users, build around:
- explicit update type routing
- state-aware handlers
- defensive payload checks
- separate flows for messages, callbacks, and edits
Do that, and your bot will stop feeling like a demo and start behaving like a product.