Create your own
Lesson illustration

Error Handling with grammY's Error Boundary

Welcome back. In our last lesson, you successfully launched your first bot and made it interactive. It's now online, listening for messages and echoing them back, which is a fantastic milestone. However, as any experienced developer knows, code that works in the "happy path" is only the first step. Right now, our bot is somewhat fragile. An unexpected error in one of our handlers could bring the entire process to a halt.

This lesson is all about making your bot more resilient. We will implement robust error handling using grammY's built-in mechanisms. The goal is to catch and log any runtime errors without crashing the bot, ensuring it remains available and responsive. We'll do this by implementing what grammY calls an "error boundary"—a protective layer that intercepts errors and allows you to handle them gracefully.

The Problem: A Brittle Bot

Let's first demonstrate why error handling is so critical. We'll modify our bot to deliberately throw an error under a specific condition. This will simulate a real-world scenario where a piece of logic fails—perhaps an external API is down, a database query fails, or there's just a plain old bug.

In your index.ts file, update your message handler to throw an error if it receives the message "crash".

// index.ts

// ... (imports and bot setup)

// Listen for text messages.
bot.on("message:text", (ctx) => {
  // Add this check to simulate an error.
  if (ctx.message.text === "crash") {
    throw new Error("This is a simulated crash!");
  }

  // The original echo logic.
  console.log(`Received message from ${ctx.from.first_name}: "${ctx.message.text}"`);
  ctx.reply(`Echo: ${ctx.message.text}`);
});

// ... (bot.start() call)

Now, save the file and run your bot again with bun run index.ts. Go to your Telegram client and send it the message "crash".

You will immediately see the bot process in your terminal terminate, likely with an unhandled exception trace. The bot is now offline. It won't respond to any further messages until you restart it manually. This is clearly not desirable for any application, let alone a bot that's supposed to be always available.

Implementing a Global Error Boundary with bot.catch

To solve this, we need to catch these errors. grammY provides a simple and powerful way to do this for long-polling bots: the bot.catch() method. This method installs a global error handler that acts as a final safety net for your entire bot. It's the outermost error boundary, ensuring that no unhandled exception from your middleware can crash the application.

The official grammY documentation explains this concept well.

Error Handling | grammY

This guide covers the fundamentals of handling errors in grammY. We'll focus on the primary method recommended for bots running on long polling.

Start by reading the introduction to get an overview of the main error types. Then, pay close attention to the section on Long Polling. The documentation strongly advises installing your own handler, which is exactly what we're about to do. Note that the code example in the documentation is slightly malformed; we will use a corrected version in our code.

As you've read, installing this handler is crucial. Let's add it to your index.ts file. This handler should be placed right after you create the bot instance but before you call bot.start().

import { Bot } from "grammy";

// ... (token setup)

const bot = new Bot(token);

// Install the global error handler.
// This is our main error boundary.
bot.catch((err) => {
  const ctx = err.ctx;
  console.error(`Error while handling update ${ctx.update.update_id}:`);
  console.error(err);
});

// ... (your bot.on() handlers)

bot.start();
// ...

Run the bot again. This time, when you send the "crash" message, observe your terminal. You will see the error logged, including the update ID that caused it, but the process will not exit. The bot remains online. If you send it another message (e.g., "hello"), it will happily echo it back. You have successfully made your bot resilient to runtime errors.

Dissecting the BotError Object

Now that we are catching errors, let's look more closely at what we're catching. The err object passed to bot.catch is an instance of BotError. This is a special wrapper object provided by grammY that contains two vital properties:

  1. err.ctx: The Context object for the update that caused the error. This is incredibly valuable for debugging, as it tells you exactly which user, in which chat, sent which message that triggered the failure.
  2. err.error: The original error that was thrown. This could be a generic Error object, or one of grammY's more specific error types.

Having both the context and the original error allows for sophisticated logging and error handling. For instance, you could even use err.ctx.reply() to send a message back to the user informing them that something went wrong.

While our simple throw new Error(...) is one possibility, errors in a Telegram bot often fall into two specific categories related to API communication. It's good practice to handle these distinctly.

Error Handling | grammY

Let's dig into the specific types of errors you'll encounter. This part of the guide details the three main error classes in grammY, which will help us refine our error handler.

Read the sections detailing The BotError Object, The GrammyError Object, and The HttpError Object. Understanding the difference between a GrammyError (an issue with your API request, like bad parameters) and an HttpError (a network issue preventing connection to Telegram) is key to writing professional-grade error handling.

Armed with this knowledge, we can upgrade our bot.catch handler to be more intelligent. By using instanceof, we can check the type of err.error and provide more specific and useful log messages. This is a common pattern in robust TypeScript applications.

Update your index.ts file with this improved handler. Make sure to add GrammyError and HttpError to your import from the grammy package.

// index.ts
import { Bot, GrammyError, HttpError } from "grammy";

// ... (token setup)

const bot = new Bot(token);

// Install the refined error handler.
bot.catch((err) => {
  const ctx = err.ctx;
  console.error(`Error while handling update ${ctx.update.update_id}:`);
  const e = err.error;

  if (e instanceof GrammyError) {
    console.error("Error in request:", e.description);
  } else if (e instanceof HttpError) {
    console.error("Could not contact Telegram:", e);
  } else {
    console.error("Unknown error:", e);
  }
});

// Handlers for messages and commands...
bot.on("message:text", (ctx) => {
  if (ctx.message.text === "crash") {
    throw new Error("This is a simulated crash!");
  }
  console.log(`Received message from ${ctx.from.first_name}: "${ctx.message.text}"`);
  ctx.reply(`Echo: ${ctx.message.text}`);
});


console.log("Bot has been started and is listening for messages...");
bot.start();

Your bot now not only survives errors but also provides categorized, actionable logs depending on the nature of the failure. This is a significant step towards a production-ready application.

The bot.catch handler serves as your main error boundary. For your goal of building complex "tech UIs," it's worth noting that grammY also allows for more granular error boundaries around specific features, but for now, this global handler provides the resilience we need.

Conclusion

In this lesson, you've fortified your bot against runtime failures. By implementing a global error boundary with bot.catch, you have ensured that unexpected exceptions will be logged without taking the entire bot offline.

Key Takeaways:

  • An unhandled error in a grammY handler will crash a bot running via bot.start().
  • The bot.catch() method installs a global error boundary that catches all middleware errors, preventing crashes.
  • The BotError object passed to the handler provides access to both the update ctx and the original error.
  • Differentiating between GrammyError (API logic errors), HttpError (network errors), and generic errors allows for more precise logging and handling.

Your bot is now robust against internal errors. However, what about external events, like when you stop the process with Ctrl+C in your terminal or when a cloud platform sends a termination signal? Right now, the bot stops abruptly. In our next lesson, we will implement a "graceful shutdown" mechanism to handle these situations cleanly.

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

Sign up