# Telego Handler Groups: Organizing Routes, Predicates, and Middleware in Telegram Bots

> Streamline your Telegram bot with Telego handler groups. Organize routes predicates and middleware for modular architecture and deterministic routing. Discover how in this article.

- Repository: [Artem Yadelskyi/telego](https://github.com/mymmrac/telego)
- Tags: tutorial
- Published: 2026-03-07

---

**Telego handler groups provide a hierarchical tree structure in the `telegohandler` package that bundles related routes, predicates, and middleware, enabling modular bot architecture with deterministic routing order.**

The `mymmrac/telego` library provides a robust `telegohandler` package for building HTTP‑style processing pipelines for Telegram updates. While flat handler registries suffice for simple bots, production applications require **Telego handler groups** to organize routes into logical, reusable hierarchies that scope middleware and predicates to specific features.

## Core Architecture of Handler Groups

The implementation in [`telegohandler/handler_group.go`](https://github.com/mymmrac/telego/blob/main/telegohandler/handler_group.go) defines the tree structure that powers routing.

### HandlerGroup Structure

The `HandlerGroup` struct acts as a node in the routing tree. It maintains a slice of routes (`[]route`) and tracks its `parent` group, enabling traversal back up the tree after a subgroup finishes processing. This parent reference is critical for the hierarchical execution model implemented in `HandleUpdate`.

### Route Internals

Each `route` struct holds the matching logic and execution target:

- **`predicates`**: A slice of `Predicate` functions (`func(context.Context, telego.Update) bool`) that filter updates
- **`handler`**: The concrete `Handler` to execute
- **`group`**: Optional pointer to a nested `*HandlerGroup`
- **Parent group reference** for context management

A route without predicates always matches, while middleware is implemented as a handler that calls `ctx.Next(update)` to continue the chain.

### Execution Context

The `Context.Next` method in [`telegohandler/context.go`](https://github.com/mymmrac/telego/blob/main/telegohandler/context.go) drives the execution flow. It creates a fresh `Context` for each update, initializes the execution stack with `[-1]`, and iterates over routes. When encountering a subgroup, it evaluates predicates, descends into the group, pushes a new stack entry, and continues scanning. After a handler returns, the stack unwinds to resume parent group processing.

## Building Handler Groups in Practice

Constructing a handler group involves chaining methods on the `BotHandler` or parent groups. The `Group` method creates a new `HandlerGroup` with specified predicates, while `Use` registers middleware and `Handle` registers terminal handlers.

```go
// Create the root bot handler attached to the update channel
bh, _ := th.NewBotHandler(bot, updates)

// Global middleware runs for every update before any group logic
bh.Use(th.PanicRecovery())
bh.Use(func(ctx *th.Context, upd telego.Update) error {
    fmt.Println("global middleware")
    return ctx.Next(upd) // continue processing
})

// Create a subgroup that only reacts to messages containing "task"
taskGroup := bh.Group(th.TextContains("task"))

// Group-specific middleware runs after global ones but before handlers
taskGroup.Use(func(ctx *th.Context, upd telego.Update) error {
    fmt.Println("group middleware")
    // Short-circuit if message is too long
    if len(upd.Message.Text) > 100 {
        return nil // stop processing
    }
    return ctx.Next(upd)
})

// Terminal handler for the task group
taskGroup.HandleMessage(func(ctx *th.Context, msg telego.Message) error {
    fmt.Println("handling task:", msg.Text)
    return nil
})

```

The `bh.Group` call creates a new `HandlerGroup` with `parent` pointing to `bh`'s root group. Middleware appended via `taskGroup.Use` executes only when the group's predicates match, keeping logic modular and reusable.

## Execution Flow Deep Dive

Understanding the traversal algorithm clarifies how Telego handler groups achieve deterministic routing.

### Depth Calculation and Stack Initialization

Before processing begins, `HandlerGroup.depth` walks the tree to determine maximum nesting levels. This value pre-allocates the execution stack in `HandleUpdate`, which initializes `stack []int` with `[-1]` as the starting index.

### Update Processing Steps

1. **Entry Point**: `BotHandler.Start()` receives an update and invokes `rootGroup.HandleUpdate` in [`telegohandler/handler_group.go`](https://github.com/mymmrac/telego/blob/main/telegohandler/handler_group.go).

2. **Context Creation**: `HandleUpdate` instantiates a `Context` with `finalGroup` pointing to the root and the pre-allocated stack.

3. **Route Iteration**: `Context.Next` iterates over `c.group.routes`. For each route:
   - Evaluates all `predicates` against the update
   - If predicates pass and `route.handler` is set, invokes the handler
   - If predicates pass and `route.group` is set, descends into the subgroup

4. **Subgroup Descent**: When entering a subgroup, `Context` sets `c.group = subGroup`, pushes the current route index to `stack`, and resets the iteration index to continue inside the nested group.

5. **Handler Return**: After a handler executes, control returns to `Context.Next`, which checks for remaining routes in the current group. If none exist, it unwinds the stack to resume parent group processing.

6. **Short-Circuiting**: Middleware can halt execution by returning without calling `ctx.Next(update)`, preventing downstream routes from processing the update.

The algorithm guarantees that **only the first matching handler** in a group executes, while middleware chains can modify the update or terminate the flow at any level.

## Practical Patterns and Examples

These patterns from [`examples/handler_groups_and_middleware/main.go`](https://github.com/mymmrac/telego/blob/main/examples/handler_groups_and_middleware/main.go) demonstrate common use cases for Telego handler groups.

### Global Middleware with Terminal Handler

Apply logging or recovery to all updates before routing to specific handlers.

```go
bh.Use(func(ctx *th.Context, upd telego.Update) error {
    log.Println("received update:", upd.UpdateID)
    return ctx.Next(upd) // forward to next route
})

bh.HandleMessage(func(ctx *th.Context, msg telego.Message) error {
    _, _ = ctx.Bot().SendMessage(tu.Messagef(
        tu.ID(msg.Chat.ID), "Echo: %s", msg.Text))
    return nil
}, th.AnyMessage())

```

### Nested Groups with Authentication

Scope sensitive commands to authorized users using group-level middleware.

```go
admin := bh.Group(th.CommandEqual("admin"))          // matches only /admin commands
admin.Use(func(ctx *th.Context, upd telego.Update) error {
    if !isAdmin(upd.Message.From.ID) {
        return nil // silently ignore non-admins
    }
    return ctx.Next(upd)
})

admin.HandleMessage(func(ctx *th.Context, msg telego.Message) error {
    _, _ = ctx.Bot().SendMessage(tu.Message(tu.ID(msg.Chat.ID),
        "Welcome, admin!"))
    return nil
})

```

### Early-Exit Middleware for Rate Limiting

Prevent downstream processing for rate-limited users without modifying handlers.

```go
bh.Use(func(ctx *th.Context, upd telego.Update) error {
    if limited(upd.Message.From.ID) {
        // Do not call ctx.Next – stops processing for this update
        return nil
    }
    return ctx.Next(upd)
})

```

## Summary

- **Telego handler groups** in `mymmrac/telego` implement a hierarchical tree structure for organizing Telegram bot routes, defined in [`telegohandler/handler_group.go`](https://github.com/mymmrac/telego/blob/main/telegohandler/handler_group.go).
- Each `HandlerGroup` maintains a slice of routes and a `parent` reference, enabling nested middleware scoping and deterministic routing priority.
- The `route` struct combines **predicates** (filtering functions), **handlers** (terminal processing), and optional **subgroups** for recursive traversal.
- Execution flows through `HandleUpdate` and `Context.Next`, using a pre-allocated stack based on `depth()` calculations to manage tree traversal.
- Middleware short-circuits the chain by returning without calling `ctx.Next`, while only the first matching handler per group executes.

## Frequently Asked Questions

### What is the difference between a handler and middleware in Telego?

In the `telegohandler` package, both handlers and middleware conform to the `Handler` function signature (`func(context.Context, telego.Update) error`). The critical distinction is execution flow: **middleware** must call `ctx.Next(update)` to pass control to the next route in the chain, while a **terminal handler** returns directly without invoking `ctx.Next`, finalizing processing for that branch. Middleware is registered via `Use()`, while terminal handlers use methods like `HandleMessage()` or `Handle()`.

### How do predicates work in handler groups?

**Predicates** are functions of type `func(context.Context, telego.Update) bool` defined in [`telegohandler/handler_group.go`](https://github.com/mymmrac/telego/blob/main/telegohandler/handler_group.go). Each `route` stores a slice of predicates that must all evaluate to `true` for the route to match. When `Context.Next` processes an update, it evaluates predicates before invoking handlers or descending into subgroups. A route with no predicates always matches. Common predicates include `th.TextContains()`, `th.CommandEqual()`, and `th.AnyMessage()`, which can be combined using `th.And()` or `th.Or()`.

### Can handler groups be nested multiple levels deep?

Yes, the `HandlerGroup` struct supports arbitrary nesting through its `parent` field and the `Group()` method. When you call `bh.Group(predicate)`, the method creates a new `HandlerGroup` with its `parent` set to the current group and appends it as a route. The `depth()` method calculates the maximum nesting level by walking parent references, and `HandleUpdate` pre-allocates an execution stack based on this depth. You can create subgroups for admin commands, feature modules, or rate-limited endpoints, each with isolated middleware chains.

### How do I stop processing an update in middleware?

To short-circuit the processing chain, return `nil` (or an error) from your middleware **without** calling `ctx.Next(update)`. The `Context.Next` method only continues to subsequent routes if the current handler explicitly invokes `ctx.Next`. This pattern is commonly used for authentication checks, rate limiting, or spam filtering where you want to prevent the update from reaching downstream handlers. For example, if a user exceeds rate limits, simply return `nil` to drop the update silently.