← back to blog

Logging a bot's errors somewhere you'll actually read

The bot works until it doesn’t

You build a Telegram bot, run it with python bot.py, watch the console for a minute, see messages coming through, and close the laptop. Three days later a user tells you the bot stopped replying. You SSH in, and the process is either dead or sitting there silently, having swallowed an exception two days ago and never said a word about it.

This is what happens to any long-running process when nobody decided in advance where its errors go. But Telegram bots make it worse than most, because they tend to run unattended for weeks, they talk to an API that changes behavior under load (flood waits, timeouts, connection resets), and a lot of people running them are doing it for the first time and never think about logging until something has already gone wrong.

Where errors actually go by default

If you start a bot with python bot.py in a terminal and then close that terminal, the process usually dies with it, unless you did something to detach it (nohup, screen, tmux, or a process manager). If you run it under systemd, its stdout and stderr go into the journal, which you can read with journalctl -u yourbot, but only if you know to look, and only for as long as the journal retains it, which on a lot of default configs is a rolling window measured in days, not months.

If you run it in Docker, docker logs shows you stdout until the container restarts or gets pruned, at which point that history is gone unless you configured a logging driver to send it somewhere durable.

None of these are wrong setups. The problem is that “the error is in there somewhere” is not the same as “I will see it.” You need a step where something actively pushes the error in front of you, rather than a step where the error passively waits for you to go looking.

A print(f"error: {e}") inside a try/except feels like you’ve handled it, but it only helps if you’re staring at that terminal when it happens. It doesn’t get a timestamp unless you add one, it doesn’t get written to disk unless stdout is redirected somewhere, and it gives you no way to tell a routine warning apart from something that needs you right now.

Python’s built-in logging module solves the mechanical part of this cheaply. It gives you levels (DEBUG, INFO, WARNING, ERROR, CRITICAL), timestamps, and the ability to send the same message to more than one destination at once, a file and a console, for example, without changing your code. Setting it up is a few lines at the top of your entry point:

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s: %(message)s",
    handlers=[
        logging.FileHandler("bot.log"),
        logging.StreamHandler(),
    ],
)
logger = logging.getLogger(__name__)

From there, logger.exception("failed to send message") inside an except block logs the message and the full traceback, which is the part people forget. Knowing that “something failed” is much less useful than knowing which line it failed on and what the exception actually was.

Catch the exception, then do something with it

Most bot frameworks (aiogram, python-telegram-bot, pyrogram) let you register a global error handler that catches anything your individual handlers didn’t. This matters because a single unhandled exception in one handler shouldn’t take down the whole bot, but if you don’t register anything, that’s exactly what happens with some frameworks and update loops.

A global handler gives you one place to decide what happens to every uncaught error: log it, and then decide whether it’s worth telling a human about right now or whether it can just sit in the file for later. Not every exception deserves your attention. A single user sending malformed input that trips a ValueError in your parsing code is not the same class of problem as your bot losing its connection to Telegram’s API entirely. Treating both the same way, either by ignoring both or alerting on both, trains you to ignore the alerts.

Split proxy errors from bot errors

This is the part that’s specific to how a lot of bots actually run. If your bot connects through a proxy, whether that’s because you’re running several bots from IPs that shouldn’t collide, or because you’re keeping bot traffic separate from other traffic on the same box, a connection failure can come from two different places and your logs need to be able to tell them apart.

A ConnectionError or timeout when calling Telegram’s API could mean Telegram is having a rough moment, or it could mean the proxy in front of your bot dropped, rotated, or started refusing connections. If your log line just says “request failed,” you’ll spend time debugging your bot’s code for a problem that’s actually sitting one layer down in the network path. Log the proxy address (not the credentials) alongside connection errors, and if you can, log the HTTP status or exception type separately from a generic “something went wrong” string. When a proxy dies, you want your log to point at the proxy, not send you rereading your handler code for a bug that isn’t there.

The same goes for rate limiting. Telegram’s API returns a 429 with a retry_after value when you’re sending too fast, and that’s a completely different situation from a genuine crash. If your logging treats a flood wait the same as an unhandled exception, you’ll get paged (or you’ll page yourself) for something that resolves on its own in a few seconds.

Send the important ones to a chat you actually check

A log file is good for history and for debugging after the fact, but it doesn’t interrupt you. For the errors that actually need a human, the simplest reliable pattern is to have your bot send itself a message. Set up a private Telegram chat or channel just for this, get its chat ID, and have your error handler call bot.send_message() to that chat whenever something crosses your “this needs attention” threshold.

This works because you’re already carrying Telegram in your pocket, so an error that shows up as a message there gets seen the same way a message from a friend would. It also costs nothing extra to set up since you already have a bot and a Bot API token; you’re just pointing it at a second chat instead of only replying to users.

Keep this channel for real problems, not every warning. If you dump every INFO line into it, you’ll mute the chat within a week and be back to square one. A reasonable split is: everything goes to the file, only ERROR and above goes to the chat, and anything you can predict and recover from automatically (flood waits, a single retry succeeding) doesn’t need to alert at all.

Keep the file too, and rotate it

Don’t rely on the alert chat as your only record, because it won’t have context, sequence, or the quieter warnings that would have told you something was building up before it broke. Keep the file. Use logging.handlers.RotatingFileHandler or TimedRotatingFileHandler instead of a plain FileHandler so the file doesn’t grow forever and eat disk space on a box that’s also running other things. A few megabytes with a handful of backups is enough for most single-bot setups; you just want something to grep through when a user says “it broke yesterday around 3pm.” Turning it into an analytics pipeline is a separate project with separate tooling.

What not to log

Log the exception type, the traceback, the handler name, and enough context to reproduce the problem. Don’t log the bot token, don’t log full proxy credentials, and think twice before logging full user message content, especially if your bot handles anything remotely personal. A log file that leaks the same things you’d protect in your code is a liability sitting on disk, and it’s an easy thing to get wrong when you’re in a hurry adding a debug line and forget to strip it back out before shipping.

A working setup, roughly

Put the pieces together and it looks like this: logging configured once at startup with a file handler and a console handler, a global error handler in your framework that catches anything unhandled, a distinction in your log messages between network and proxy failures versus application bugs, and a small function that sends ERROR-level messages to a private alert chat. None of this is exotic. It’s maybe thirty lines of code total, and the difference it makes is that the next time something breaks, you find out from a message on your phone instead of from a user asking why the bot’s gone quiet.

If you’re running a bot on hosting or proxies where you’d rather not guess whether a failure is yours or the infrastructure’s, that’s the kind of thing we deal with daily at telegramvault.org.

Get new guides and videos first — join the Telegram channel.

need infra for this today?