Skip to main content
grammY provides three main error types to help you handle different failure scenarios in your bot.

BotError<C>

Thrown when middleware throws an error. Wraps the original error and provides access to the context.
class BotError<C extends Context = Context> extends Error {
  constructor(
    public readonly error: unknown,
    public readonly ctx: C
  )
}

Properties

  • error: The original error that was thrown by your middleware
  • ctx: The context object being processed when the error occurred
  • message: A descriptive error message
  • name: Always "BotError"
  • stack: Stack trace from the original error (if available)

When it occurs

BotError is thrown whenever an error occurs during middleware execution:
bot.on('message', (ctx) => {
  throw new Error('Something went wrong!')  // Will be wrapped in BotError
})

bot.on('message', async (ctx) => {
  await riskyOperation()  // If this throws, it becomes BotError
})

Handling BotError

Use bot.catch() to install an error handler:
import { BotError } from 'grammy'

bot.catch((err: BotError) => {
  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)
  }
})

Accessing context from errors

The ctx property lets you access update information when an error occurs:
bot.catch(async (err: BotError) => {
  const ctx = err.ctx
  
  // Notify user about the error
  if (ctx.chat) {
    await ctx.reply('Sorry, an error occurred while processing your request.')
  }
  
  // Log detailed information
  console.error('Error details:', {
    updateId: ctx.update.update_id,
    userId: ctx.from?.id,
    chatId: ctx.chat?.id,
    error: err.error
  })
})

Example: Retry on error

bot.catch(async (err: BotError) => {
  if (err.error instanceof GrammyError) {
    if (err.error.error_code === 429) {
      // Rate limited - retry after delay
      const retryAfter = err.error.parameters.retry_after ?? 30
      console.log(`Rate limited. Retrying after ${retryAfter}s`)
      await new Promise(resolve => setTimeout(resolve, retryAfter * 1000))
      // Re-handle the update
      await bot.handleUpdate(err.ctx.update)
    }
  }
})

GrammyError

Thrown when the Telegram Bot API returns an error response.
class GrammyError extends Error implements ApiError {
  public readonly ok: false = false
  public readonly error_code: number
  public readonly description: string
  public readonly parameters: ResponseParameters
  
  constructor(
    message: string,
    err: ApiError,
    public readonly method: string,
    public readonly payload: Record<string, unknown>
  )
}

Properties

  • error_code: Telegram’s error code (e.g., 400, 404, 429)
  • description: Human-readable error description from Telegram
  • parameters: Additional parameters (e.g., retry_after for rate limits)
  • method: The API method that was called (e.g., "sendMessage")
  • payload: The parameters passed to the API method
  • ok: Always false

Common Error Codes

CodeDescription
400Bad Request - invalid parameters
401Unauthorized - invalid bot token
403Forbidden - no permission to perform action
404Not Found - resource doesn’t exist
409Conflict - bot is already running elsewhere
429Too Many Requests - rate limited
500Internal Server Error - Telegram server issue

When it occurs

GrammyError is thrown when Telegram’s API returns an error:
try {
  await ctx.reply('Hello!')
} catch (err) {
  if (err instanceof GrammyError) {
    console.error('Telegram API error:', err.description)
  }
}

Example: Handling specific errors

import { GrammyError } from 'grammy'

try {
  await bot.api.sendMessage(chatId, text)
} catch (err) {
  if (err instanceof GrammyError) {
    switch (err.error_code) {
      case 400:
        console.error('Bad request:', err.description)
        break
      
      case 403:
        console.error('Bot was blocked by user')
        // Remove user from database
        break
      
      case 429:
        const retryAfter = err.parameters.retry_after
        console.error(`Rate limited. Retry after ${retryAfter}s`)
        break
      
      default:
        console.error('Telegram error:', err)
    }
  }
}

Example: Handling message deletion

try {
  await ctx.deleteMessage()
} catch (err) {
  if (err instanceof GrammyError) {
    if (err.description.includes('message to delete not found')) {
      console.log('Message already deleted')
    } else if (err.description.includes("message can't be deleted")) {
      console.log('Message is too old to delete')
    } else {
      throw err  // Re-throw unknown errors
    }
  }
}

Example: Handling bot blocking

bot.on('message', async (ctx) => {
  try {
    await ctx.reply('Hello!')
  } catch (err) {
    if (err instanceof GrammyError) {
      if (err.error_code === 403) {
        console.log(`User ${ctx.from.id} blocked the bot`)
        // Update user status in database
        await db.users.update(ctx.from.id, { blocked: true })
      }
    }
  }
})

ResponseParameters

Some errors include additional parameters:
interface ResponseParameters {
  migrate_to_chat_id?: number      // Group migrated to supergroup
  retry_after?: number             // Seconds to wait before retry
}
Example:
try {
  await ctx.reply('Message')
} catch (err) {
  if (err instanceof GrammyError) {
    if (err.parameters.migrate_to_chat_id) {
      const newChatId = err.parameters.migrate_to_chat_id
      console.log(`Chat migrated to ${newChatId}`)
      // Update chat ID in database
    }
  }
}

HttpError

Thrown when the HTTP request to Telegram’s servers fails.
class HttpError extends Error {
  constructor(
    message: string,
    public readonly error: unknown
  )
}

Properties

  • error: The underlying network error
  • message: Descriptive error message
  • name: Always "HttpError"

When it occurs

HttpError is thrown when:
  • Network connection fails
  • DNS resolution fails
  • Request times out
  • Connection is aborted
  • API transformer function throws
import { HttpError } from 'grammy'

try {
  await bot.api.sendMessage(chatId, 'Hello')
} catch (err) {
  if (err instanceof HttpError) {
    console.error('Network error:', err.error)
    // Retry logic here
  }
}

Example: Retry on network failure

import { HttpError } from 'grammy'

async function sendWithRetry(chatId: number, text: string, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await bot.api.sendMessage(chatId, text)
    } catch (err) {
      if (err instanceof HttpError) {
        console.log(`Network error. Retry ${i + 1}/${maxRetries}`)
        if (i === maxRetries - 1) throw err  // Last retry failed
        await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
      } else {
        throw err  // Not a network error
      }
    }
  }
}

Error Handling Patterns

Global Error Handler

Handle all errors in one place:
import { BotError, GrammyError, HttpError } from 'grammy'

bot.catch((err: BotError) => {
  const e = err.error
  
  if (e instanceof GrammyError) {
    console.error('API Error:', e.description)
    
    if (e.error_code === 401) {
      console.error('Invalid bot token!')
      process.exit(1)
    }
  } else if (e instanceof HttpError) {
    console.error('Network error:', e)
  } else {
    console.error('Unknown error:', e)
  }
})

Local Error Handling

Handle errors in specific middleware:
bot.command('start', async (ctx) => {
  try {
    await ctx.reply('Welcome!')
  } catch (err) {
    console.error('Failed to send welcome message:', err)
    // Don't crash the bot, just log it
  }
})

Error Boundaries

Create protected middleware sections:
bot.errorBoundary(
  (err) => {
    console.error('Error in admin commands:', err)
  },
  adminCommands.middleware()
)

Ignore Specific Errors

Silently handle expected errors:
bot.on('message', async (ctx) => {
  try {
    await ctx.deleteMessage()
  } catch (err) {
    if (err instanceof GrammyError) {
      // Ignore "message not found" errors
      if (!err.description.includes('message to delete not found')) {
        throw err
      }
    } else {
      throw err
    }
  }
})

Custom Error Classes

Create application-specific errors:
class UserNotFoundError extends Error {
  constructor(public userId: number) {
    super(`User ${userId} not found`)
    this.name = 'UserNotFoundError'
  }
}

bot.on('message', async (ctx) => {
  const user = await db.findUser(ctx.from.id)
  if (!user) {
    throw new UserNotFoundError(ctx.from.id)
  }
  // ...
})

bot.catch((err: BotError) => {
  if (err.error instanceof UserNotFoundError) {
    console.log('New user:', err.error.userId)
    // Create user in database
  }
})

Error Logging

Log errors with context:
bot.catch(async (err: BotError) => {
  const errorLog = {
    timestamp: new Date().toISOString(),
    updateId: err.ctx.update.update_id,
    userId: err.ctx.from?.id,
    chatId: err.ctx.chat?.id,
    errorType: err.error?.constructor.name,
    errorMessage: err.error instanceof Error ? err.error.message : String(err.error),
    stack: err.stack
  }
  
  console.error(JSON.stringify(errorLog, null, 2))
  
  // Send to external logging service
  await logger.error(errorLog)
})

Best Practices

  1. Always set an error handler
    bot.catch((err) => {
      console.error('Bot error:', err)
    })
    
  2. Handle specific errors explicitly
    if (err instanceof GrammyError && err.error_code === 403) {
      // User blocked the bot
    }
    
  3. Don’t leak sensitive information
    // ❌ Bad - might expose token in logs
    console.error(err)
    
    // ✅ Good - sanitize error
    console.error(err.description)
    
  4. Implement retry logic for transient errors
    if (err instanceof HttpError || err.error_code >= 500) {
      // Retry
    }
    
  5. Fail gracefully
    try {
      await ctx.reply('Result')
    } catch {
      await ctx.reply('Sorry, something went wrong')
    }
    
Proper error handling makes your bot robust and reliable!

Build docs developers (and LLMs) love