Create your own
Lesson illustration

Middleware and `next()` in grammY

Welcome to a new module! In the last few lessons, we mastered building complex interactive UIs using grammY's powerful menu plugin. You saw how a single line, bot.use(mainMenu), could replace a significant amount of manual logic for handling callback queries and message edits. That single line is your entry point into one of the most powerful concepts in grammY and modern web frameworks: middleware.

Today, we begin our exploration of advanced control flow. This lesson focuses on the theoretical foundation you'll need for the rest of the module. We will dissect grammY's middleware execution model and the pivotal role of the next() function. Given your extensive background in front-end development, you've likely encountered similar middleware patterns in frameworks like Express.js or Koa.js. Our goal is to map that existing knowledge to grammY's specific implementation, giving you a deep understanding of how the framework processes updates from start to finish.

What Is Middleware? The 30,000-Foot View

At its core, middleware is a chain of functions that a request, or in our case, a Telegram Update, passes through on its journey from being received by your bot to a response being sent back. Each function in the chain gets a chance to inspect the update, modify it, add data to it, or even stop it in its tracks.

A great way to visualize this is to think of airport security. A passenger (the update) doesn't go straight to the plane (your final logic). They first go through ticketing, then baggage check, then security screening. Each checkpoint is a piece of middleware.

A simplified view of a client request passing sequentially through multiple middleware functions before a final response is generated.

This concept is almost universal in modern back-end frameworks. The following video explains middleware in the context of Express.js, and the core ideas are directly transferable to grammY.

Why Every Developer Needs to Understand Middleware

Watch this short segment from the video "Why Every Developer Needs to Understand Middleware" for a crisp, high-level analogy.

Focus on the airport analogy to solidify the concept of middleware as a series of processing layers.

In grammY, almost everything you've used so far is middleware. Handlers you register with bot.command("start", ...) or bot.on("message:text", ...) are just middleware functions attached to specific filters. The menu plugin you installed with bot.use(mainMenu) is also a sophisticated piece of middleware.

The Middleware Function: ctx and next

A grammY middleware function has a specific signature. Let's look at the formal definition from the documentation.

type MiddlewareFn = (ctx: Context, next: NextFunction) => MaybePromise<unknown>;
type NextFunction = () => Promise<void>;

You are already well-acquainted with the first parameter, ctx, the all-important Context object that holds the update, API methods, and other useful data.

The second parameter, next, is the key to understanding the execution flow. It is a function that, when called, passes control to the next middleware in the processing chain. If a middleware function doesn't call next(), the chain is broken, and no further middleware will be executed for that update. This allows middleware to act as a gatekeeper or to handle the update completely.

The following reading from the official grammY documentation formally introduces the middleware stack and the next function.

Middleware | grammY

This guide explains how grammY organizes middleware into a stack and the importance of the order in which you register your handlers.

Read the introduction and the section on "The Middleware Stack". Pay close attention to the MiddlewareFn type definition and the explanation of how next is used to invoke downstream middleware. The example showing why the order of bot.on(":text") and bot.command("start") matters is crucial.

As you can see, the order of registration defines the order of execution. A bot.use() handler registered at the top will see every update first, making it ideal for cross-cutting concerns like logging, session management, or authentication.

The "Russian Doll" Execution Model

Here is where the real power of middleware becomes apparent. The next() function returns a Promise, which means you should always await it. This async/await structure creates an execution model that can be visualized as nested Russian dolls or an onion.

A piece of middleware can execute code before passing control down the chain, and it can also execute code after the entire downstream chain has completed.

This diagram illustrates how a request flows "in" through layers of middleware (pre-processing) and the response flows "out" in reverse order (post-processing), with each layer wrapping the one inside it.

Let's break down the flow:

  1. An update enters Middleware A.
  2. Middleware A runs its "pre-processing" code.
  3. Middleware A calls await next(), passing control to Middleware B.
  4. Middleware B runs its "pre-processing" code.
  5. Middleware B calls await next(), passing control to your main handler (e.g., bot.command).
  6. Your main handler runs and completes.
  7. Control returns to Middleware B, which now executes its "post-processing" code (the code after await next()).
  8. When Middleware B completes, control returns to Middleware A, which runs its "post-processing" code.

This pattern is extremely useful. For example, you can start a timer before await next() and stop it after, allowing you to measure the response time of your handlers.

Let's see this in action by studying a practical example and a critical warning from the grammY documentation.

Middleware | grammY

This part of the documentation provides a perfect example of the "Russian Doll" pattern and contains a vital warning about promise handling.

First, carefully read the section "Writing Custom Middleware". The responseTime function is a canonical example of this execution model. Notice how it takes a timestamp, calls await next(), and then takes another timestamp to calculate the duration. Next, and this is extremely important, read the DANGER box titled "Always Make Sure to await next!". As an experienced TypeScript developer, you understand the hazards of unhandled promises, and this section explains the severe and unpredictable consequences of forgetting to await the next() call in grammY.

This before-and-after execution pattern is a common feature in many middleware systems. The following video explains it very clearly in the context of Express.js, and the logic is identical.

Learn Express Middleware In 14 Minutes

This video on Express middleware has a great visual demonstration of the execution flow around the next() call.

Watch the segment from after to before. It shows how code placed before the next() call runs first, and code placed after runs once the downstream middleware has finished, perfectly illustrating the "Russian Doll" model we discussed.

Conclusion

In this lesson, we've pulled back the curtain on how grammY handles updates. You've learned that middleware is not just a simple linear chain, but a powerful, layered execution model that gives you fine-grained control over the entire update-processing lifecycle.

Key Takeaways:

  • Middleware in grammY are functions with the signature (ctx, next) that process updates in a chain.
  • The order of middleware registration is critical as it defines the execution order.
  • The next() function passes control to the subsequent middleware in the chain. Not calling it terminates the processing for that branch.
  • The await next() pattern enables the "Russian Doll" model, allowing code to run both before and after the downstream handlers, perfect for tasks like logging, performance measurement, and error handling.
  • Forgetting to await next() can lead to unpredictable behavior and crashes, so using a linter to catch floating promises is highly recommended.

Now that you have a solid theoretical understanding of the middleware model, you're ready to apply it. In our next lesson, you'll write your first custom middleware: a function for logging all incoming updates, putting these powerful concepts into practice.

Can't find a good explanation? Sign up and we'll make it for you

Sign up