aiogram/examples/error_handling.py
Andrew df7b16d5b3
Some checks failed
Tests / tests (macos-latest, 3.10) (push) Has been cancelled
Tests / tests (macos-latest, 3.11) (push) Has been cancelled
Tests / tests (macos-latest, 3.12) (push) Has been cancelled
Tests / tests (macos-latest, 3.13) (push) Has been cancelled
Tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
Tests / tests (ubuntu-latest, 3.11) (push) Has been cancelled
Tests / tests (ubuntu-latest, 3.12) (push) Has been cancelled
Tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
Tests / tests (windows-latest, 3.10) (push) Has been cancelled
Tests / tests (windows-latest, 3.11) (push) Has been cancelled
Tests / tests (windows-latest, 3.12) (push) Has been cancelled
Tests / tests (windows-latest, 3.13) (push) Has been cancelled
Tests / pypy-tests (macos-latest, pypy3.10) (push) Has been cancelled
Tests / pypy-tests (macos-latest, pypy3.11) (push) Has been cancelled
Tests / pypy-tests (ubuntu-latest, pypy3.10) (push) Has been cancelled
Tests / pypy-tests (ubuntu-latest, pypy3.11) (push) Has been cancelled
EOL of Py3.9 (#1726)
* Drop py3.9 and pypy3.9

Add pypy3.11 (testing) into `tests.yml`

Remove py3.9 from matrix in `tests.yml`

Refactor not auto-gen code to be compatible with py3.10+, droping ugly 3.9 annotation.

Replace some `from typing` imports to `from collections.abc`, due to deprecation

Add `from __future__ import annotations` and `if TYPE_CHECKING:` where possible

Add some `noqa` to calm down Ruff in some places, if Ruff will be used as default linting+formatting tool in future

Replace some relative imports to absolute

Sort `__all__` tuples in `__init__.py` and some other `.py` files

Sort `__slots__` tuples in classes

Split raises into `msg` and `raise` (`EM101`, `EM102`) to not duplicate error message in the traceback

Add `Self` from `typing_extenstion` where possible

Resolve typing problem in `aiogram/filters/command.py:18`

Concatenate nested `if` statements

Convert `HandlerContainer` into a dataclass in `aiogram/fsm/scene.py`

Bump tests docker-compose.yml `redis:6-alpine` -> `redis:8-alpine`

Bump tests docker-compose.yml `mongo:7.0.6` -> `mongo:8.0.14`

Bump pre-commit-config `black==24.4.2` -> `black==25.9.0`

Bump pre-commit-config `ruff==0.5.1` -> `ruff==0.13.3`

Update Makefile lint for ruff to show fixes

Add `make outdated` into Makefile

Use `pathlib` instead of `os.path`

Bump `redis[hiredis]>=5.0.1,<5.3.0` -> `redis[hiredis]>=6.2.0,<7`

Bump `cryptography>=43.0.0` -> `cryptography>=46.0.0` due to security reasons

Bump `pytz~=2023.3` -> `pytz~=2025.2`

Bump `pycryptodomex~=3.19.0` -> `pycryptodomex~=3.23.0` due to security reasons

Bump linting and formatting tools

* Add `1726.removal.rst`

* Update aiogram/utils/dataclass.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update aiogram/filters/callback_data.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update 1726.removal.rst

* Remove `outdated` from Makefile

* Add `__slots__` to `HandlerContainer`

* Remove unused imports

* Add `@dataclass` with `slots=True` to `HandlerContainer`

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-06 19:19:23 +03:00

116 lines
4 KiB
Python

import asyncio
import html
import logging
from os import getenv
from aiogram import Bot, Dispatcher, types
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.filters import (
Command,
CommandObject,
ExceptionMessageFilter,
ExceptionTypeFilter,
)
from aiogram.types import ErrorEvent
TOKEN = getenv("BOT_TOKEN")
dp = Dispatcher()
logger = logging.getLogger(__name__)
class InvalidAge(Exception):
pass
class InvalidName(Exception):
def __init__(self, message: str):
super().__init__(message)
@dp.errors(ExceptionTypeFilter(InvalidAge))
async def handle_invalid_age_exception(event: ErrorEvent, bot: Bot) -> None:
"""
This handler receives only error events with `InvalidAge` exception type.
"""
# To get the original event that caused the exception you can use `event.update` property.
# In this case it will be `Message` object.
# To get the exception itself you can use `event.exception` property.
# In this case we filter errors, so we can be sure `event.exception` is an `InvalidAge` object.
assert isinstance(event.exception, InvalidAge)
logger.error("Error caught: %r while processing %r", event.exception, event.update)
assert event.update.message is not None
chat_id = event.update.message.chat.id
# Bot instance is passed to the handler as a keyword argument.
# We can use `bot.send_message` method to send a message to the user, logging the error.
text = f"Error caught: {html.escape(repr(event.exception))}"
await bot.send_message(chat_id=chat_id, text=text)
@dp.errors(ExceptionMessageFilter("Invalid"))
async def handle_invalid_exceptions(event: ErrorEvent) -> None:
"""
This handler receives error events with "Invalid" message in them.
"""
# Because we specified `ExceptionTypeFilter` with `InvalidAge` exception type earlier,
# this handler will receive error events with any exception type except `InvalidAge` and
# only if the exception message contains "Invalid" substring.
logger.error("Error `Invalid` caught: %r while processing %r", event.exception, event.update)
@dp.message(Command("age"))
async def handle_set_age(message: types.Message, command: CommandObject) -> None:
"""
This handler receives only messages with `/age` command.
If the user sends a message with `/age` command, but the age is invalid,
the `InvalidAge` exception will be raised and the `handle_invalid_age_exception`
handler will be called.
"""
# To get the command object you can use `command` keyword argument with `CommandObject` type.
# To get the command arguments you can use `command.args` property.
age = command.args
if not age:
msg = "No age provided. Please provide your age as a command argument."
raise InvalidAge(msg)
# If the age is invalid, raise an exception.
if not age.isdigit():
msg = "Age should be a number"
raise InvalidAge(msg)
# If the age is valid, send a message to the user.
age = int(age)
await message.reply(text=f"Your age is {age}")
@dp.message(Command("name"))
async def handle_set_name(message: types.Message, command: CommandObject) -> None:
"""
This handler receives only messages with `/name` command.
"""
# To get the command object you can use `command` keyword argument with `CommandObject` type.
# To get the command arguments you can use `command.args` property.
name = command.args
if not name:
msg = "Invalid name. Please provide your name as a command argument."
raise InvalidName(msg)
# If the name is valid, send a message to the user.
await message.reply(text=f"Your name is {name}")
async def main() -> None:
# Initialize Bot instance with default bot properties which will be passed to all API calls
bot = Bot(token=TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
# And the run events dispatching
await dp.start_polling(bot)
if __name__ == "__main__":
loop = asyncio.new_event_loop()
loop.run_until_complete(main())