2026, Jan 08 03:00

aiogram 3.x FSM context injection: why 'state' is required in handler signatures and how to fix errors

Debug aiogram 3.x FSM context injection: avoid TypeError by naming the handler parameter state. See the exact fix and why storage changes don't help here.

When wiring up an aiogram 3.x bot with FSM, it’s easy to assume the framework injects the FSM context purely by type. In reality, a small naming mismatch in the handler signature can break injection and surface as a confusing runtime error. Here is a concise walkthrough of the pitfall and the exact fix.

Minimal setup that reproduces the issue

The following code uses a Redis-backed FSM storage and a router with both a CommandStart message handler and a callback query filter. The core is the handler that accepts two parameters: the update payload and the FSM context.

# tools.py
import aiogram
import aiogram.filters
import aiogram.types
import aiogram.fsm.context
import aiogram.fsm.state
from aiogram.fsm.storage.redis import RedisStorage
import redis.asyncio as redis
import json
import typing
with open("Telegram/Configs.json", "r") as fh:
    tg_settings: dict[str, typing.Any] = json.load(fh)
# main.py
import tools as kit
dispatcher = kit.aiogram.Dispatcher(
    storage=kit.RedisStorage(
        kit.redis.Redis(
            host="127.0.0.1",
            port=6379,
        )
    )
)
router = kit.aiogram.Router(name="Core")
@router.message(kit.aiogram.filters.CommandStart())
@router.callback_query(kit.aiogram.F.data == "start")
async def entry(
    payload: kit.aiogram.types.Message | kit.aiogram.types.CallbackQuery,
    State: kit.aiogram.fsm.context.FSMContext,
):
    print("/Started")
dispatcher.include_router(router)

Running a /start leads to a failure similar to:

TypeError: start() missing 1 required positional argument: 'State'

What actually goes wrong

The FSM context is injected into handlers by name. The parameter must be named state. Using a different name, including a capitalized State, prevents aiogram from providing the FSM context, so the framework calls the handler with only the event payload. Because the function signature still expects another positional argument, Python raises a TypeError complaining about the missing State parameter.

Changing the storage backend does not affect this behavior. The same error occurs even with MemoryStorage(), which confirms the issue is unrelated to Redis and fully tied to how the framework resolves the handler arguments.

It’s technically possible to fetch the context explicitly inside each handler via Dispatcher.fsm.resolve_context(), but doing so repeatedly is tedious and unnecessary when automatic injection works as intended with the correct name.

Using both @router.message and @router.callback_query in the same example does not change the root cause. The injection rule applies the same way either way.

The fix

Rename the second parameter to state. The rest of the logic remains the same.

# main.py (fixed)
import tools as kit
dispatcher = kit.aiogram.Dispatcher(
    storage=kit.RedisStorage(
        kit.redis.Redis(
            host="127.0.0.1",
            port=6379,
        )
    )
)
router = kit.aiogram.Router(name="Core")
@router.message(kit.aiogram.filters.CommandStart())
@router.callback_query(kit.aiogram.F.data == "start")
async def entry(
    payload: kit.aiogram.types.Message | kit.aiogram.types.CallbackQuery,
    state: kit.aiogram.fsm.context.FSMContext,
):
    print("/Started")
dispatcher.include_router(router)

Why this detail matters

Handler signatures are the contract the dispatcher relies on. Small mismatches in naming lead to ambiguous errors that look like parameter count issues rather than DI resolution problems. Knowing that FSM context injection depends on the exact name state helps avoid chasing false leads in storage, middleware, or decorator order.

Takeaways

Keep the FSM context parameter named state in aiogram 3.x handlers to leverage automatic injection. If you see a missing positional argument error pointing to your context argument, check its exact name before changing storage or refactoring middleware. If you ever need a manual fallback, resolving the context per handler is possible, but automatic injection is the clean and intended path.