A modern, easy-to-use, feature-rich async-ready API wrapper for Discord written in Python
—
Comprehensive error handling system covering all Discord API errors, command framework errors, interaction errors, and custom exception types with proper error recovery patterns and best practices for robust Discord bot development.
Core exception hierarchy for all Discord-related errors and failures.
class DiscordException(Exception):
"""Base exception class for all Discord-related errors."""
class ClientException(DiscordException):
"""Exception for client operation failures."""
class InvalidData(ClientException):
"""Exception for malformed or invalid data from Discord API."""
class InvalidArgument(ClientException):
"""Exception for invalid arguments passed to functions."""
class LoginFailure(ClientException):
"""Exception for bot authentication failures."""
class NoMoreItems(ClientException):
"""Exception when async iterator has no more items."""
class GatewayNotFound(DiscordException):
"""Exception when gateway URL cannot be found."""
class ConnectionClosed(ClientException):
"""Exception when WebSocket connection is closed."""
def __init__(self, socket: WebSocketClientProtocol, *, shard_id: Optional[int] = None):
"""
Initialize connection closed exception.
Parameters:
- socket: WebSocket connection
- shard_id: Shard ID if applicable
"""
code: int
reason: str
shard_id: Optional[int]
class PrivilegedIntentsRequired(ClientException):
"""Exception for missing privileged intents."""
def __init__(self, shard_id: Optional[int] = None):
"""
Initialize privileged intents exception.
Parameters:
- shard_id: Shard ID if applicable
"""
shard_id: Optional[int]
class SessionStartLimitReached(ClientException):
"""Exception when session start limit is reached."""HTTP request and API error handling for Discord REST API interactions.
class HTTPException(DiscordException):
"""Exception for HTTP request failures."""
def __init__(self, response: ClientResponse, message: Union[str, Dict[str, Any]]):
"""
Initialize HTTP exception.
Parameters:
- response: HTTP response object
- message: Error message or error data
"""
response: ClientResponse
status: int
code: int
text: str
class Forbidden(HTTPException):
"""Exception for 403 Forbidden HTTP errors."""
class NotFound(HTTPException):
"""Exception for 404 Not Found HTTP errors."""
class DiscordServerError(HTTPException):
"""Exception for 5xx server errors from Discord."""
class RateLimited(HTTPException):
"""Exception for rate limit errors (429)."""
def __init__(self, response: ClientResponse, message: Dict[str, Any]):
"""
Initialize rate limit exception.
Parameters:
- response: HTTP response
- message: Rate limit data
"""
retry_after: float
is_global: boolErrors specific to Discord interactions and UI components.
class InteractionException(DiscordException):
"""Base exception for interaction-related errors."""
class InteractionResponded(InteractionException):
"""Exception when interaction has already been responded to."""
def __init__(self, interaction: Interaction):
"""
Initialize interaction responded exception.
Parameters:
- interaction: The interaction that was already responded to
"""
interaction: Interaction
class InteractionNotResponded(InteractionException):
"""Exception when interaction has not been responded to."""
def __init__(self, interaction: Interaction):
"""
Initialize interaction not responded exception.
Parameters:
- interaction: The interaction that needs a response
"""
interaction: Interaction
class InteractionTimedOut(InteractionException):
"""Exception when interaction times out."""
def __init__(self, interaction: Interaction):
"""
Initialize interaction timeout exception.
Parameters:
- interaction: The timed out interaction
"""
interaction: Interaction
class InteractionNotEditable(InteractionException):
"""Exception when interaction response cannot be edited."""
def __init__(self, interaction: Interaction):
"""
Initialize interaction not editable exception.
Parameters:
- interaction: The non-editable interaction
"""
interaction: Interaction
class ModalChainNotSupported(InteractionException):
"""Exception when modal chaining is attempted but not supported."""
class WebhookTokenMissing(DiscordException):
"""Exception when webhook token is required but missing."""
class LocalizationKeyError(DiscordException):
"""Exception for localization key errors."""
def __init__(self, key: str):
"""
Initialize localization key error.
Parameters:
- key: The missing localization key
"""
key: strComprehensive error handling for the message-based command framework.
class CommandError(DiscordException):
"""Base exception for command-related errors."""
def __init__(self, message: str = None, *args):
"""
Initialize command error.
Parameters:
- message: Error message
- args: Additional arguments
"""
class CommandNotFound(CommandError):
"""Exception when command is not found."""
class MissingRequiredArgument(UserInputError):
"""Exception for missing required command arguments."""
def __init__(self, param: Parameter):
"""
Initialize missing argument exception.
Parameters:
- param: Missing parameter information
"""
param: Parameter
class TooManyArguments(UserInputError):
"""Exception when too many arguments are provided."""
class BadArgument(UserInputError):
"""Exception for invalid argument values or types."""
class BadUnionArgument(UserInputError):
"""Exception for failed union type conversions."""
def __init__(self, param: Parameter, converters: List[Converter], errors: List[CommandError]):
"""
Initialize bad union argument exception.
Parameters:
- param: Parameter that failed conversion
- converters: Attempted converters
- errors: Conversion errors
"""
param: Parameter
converters: List[Converter]
errors: List[CommandError]
class BadLiteralArgument(UserInputError):
"""Exception for failed literal type conversions."""
def __init__(self, param: Parameter, literals: List[Any], errors: List[CommandError]):
"""
Initialize bad literal argument exception.
Parameters:
- param: Parameter that failed conversion
- literals: Expected literal values
- errors: Conversion errors
"""
param: Parameter
literals: List[Any]
errors: List[CommandError]
class ArgumentParsingError(UserInputError):
"""Exception for general argument parsing failures."""
class UnexpectedQuoteError(ArgumentParsingError):
"""Exception for unexpected quotes in arguments."""
def __init__(self, quote: str):
"""
Initialize unexpected quote error.
Parameters:
- quote: The unexpected quote character
"""
quote: str
class InvalidEndOfQuotedStringError(ArgumentParsingError):
"""Exception for invalid end of quoted string."""
def __init__(self, char: str):
"""
Initialize invalid end of quoted string error.
Parameters:
- char: The invalid character
"""
char: str
class ExpectedClosingQuoteError(ArgumentParsingError):
"""Exception for missing closing quote."""
def __init__(self, close_quote: str):
"""
Initialize expected closing quote error.
Parameters:
- close_quote: Expected closing quote character
"""
close_quote: str
class CheckFailure(CommandError):
"""Exception when command check fails."""
class CheckAnyFailure(CheckFailure):
"""Exception when all command checks fail."""
def __init__(self, checks: List[Check], errors: List[CheckFailure]):
"""
Initialize check any failure exception.
Parameters:
- checks: Failed checks
- errors: Individual check errors
"""
checks: List[Check]
errors: List[CheckFailure]
class PrivateMessageOnly(CheckFailure):
"""Exception for DM-only commands used in guilds."""
class NoPrivateMessage(CheckFailure):
"""Exception for guild-only commands used in DMs."""
class NotOwner(CheckFailure):
"""Exception when non-owner tries to use owner-only command."""
class MissingRole(CheckFailure):
"""Exception for missing required role."""
def __init__(self, missing_role: Union[str, int]):
"""
Initialize missing role exception.
Parameters:
- missing_role: Required role name or ID
"""
missing_role: Union[str, int]
class BotMissingRole(CheckFailure):
"""Exception when bot is missing required role."""
def __init__(self, missing_role: Union[str, int]):
"""
Initialize bot missing role exception.
Parameters:
- missing_role: Required role name or ID
"""
missing_role: Union[str, int]
class MissingAnyRole(CheckFailure):
"""Exception when missing any of the required roles."""
def __init__(self, missing_roles: List[Union[str, int]]):
"""
Initialize missing any role exception.
Parameters:
- missing_roles: List of required role names or IDs
"""
missing_roles: List[Union[str, int]]
class BotMissingAnyRole(CheckFailure):
"""Exception when bot is missing any of the required roles."""
def __init__(self, missing_roles: List[Union[str, int]]):
"""
Initialize bot missing any role exception.
Parameters:
- missing_roles: List of required role names or IDs
"""
missing_roles: List[Union[str, int]]
class MissingPermissions(CheckFailure):
"""Exception for missing required permissions."""
def __init__(self, missing_permissions: List[str]):
"""
Initialize missing permissions exception.
Parameters:
- missing_permissions: List of missing permission names
"""
missing_permissions: List[str]
class BotMissingPermissions(CheckFailure):
"""Exception when bot is missing required permissions."""
def __init__(self, missing_permissions: List[str]):
"""
Initialize bot missing permissions exception.
Parameters:
- missing_permissions: List of missing permission names
"""
missing_permissions: List[str]
class NSFWChannelRequired(CheckFailure):
"""Exception for NSFW commands used in non-NSFW channels."""
class DisabledCommand(CommandError):
"""Exception when command is disabled."""
class CommandInvokeError(CommandError):
"""Exception wrapping errors during command execution."""
def __init__(self, e: Exception):
"""
Initialize command invoke error.
Parameters:
- e: Original exception that was raised
"""
original: Exception
class CommandOnCooldown(CommandError):
"""Exception when command is on cooldown."""
def __init__(self, cooldown: Cooldown, retry_after: float, type: BucketType):
"""
Initialize command on cooldown exception.
Parameters:
- cooldown: Cooldown configuration
- retry_after: Time until command can be used again
- type: Cooldown bucket type
"""
cooldown: Cooldown
retry_after: float
type: BucketType
class MaxConcurrencyReached(CommandError):
"""Exception when maximum command concurrency is reached."""
def __init__(self, number: int, per: BucketType):
"""
Initialize max concurrency exception.
Parameters:
- number: Maximum allowed concurrent uses
- per: Concurrency bucket type
"""
number: int
per: BucketType
class UserInputError(CommandError):
"""Base exception for user input errors."""
class ConversionError(CommandError):
"""Exception for type conversion failures."""
def __init__(self, converter: Converter, original: Exception):
"""
Initialize conversion error.
Parameters:
- converter: Converter that failed
- original: Original exception
"""
converter: Converter
original: Exception
class ExtensionError(DiscordException):
"""Base exception for extension-related errors."""
def __init__(self, message: str = None, *args, name: str):
"""
Initialize extension error.
Parameters:
- message: Error message
- args: Additional arguments
- name: Extension name
"""
name: str
class ExtensionAlreadyLoaded(ExtensionError):
"""Exception when extension is already loaded."""
class ExtensionNotLoaded(ExtensionError):
"""Exception when extension is not loaded."""
class NoEntryPointError(ExtensionError):
"""Exception when extension has no setup function."""
class ExtensionFailed(ExtensionError):
"""Exception when extension fails to load."""
def __init__(self, name: str, original: Exception):
"""
Initialize extension failed exception.
Parameters:
- name: Extension name
- original: Original exception
"""
original: Exception
class ExtensionNotFound(ExtensionError):
"""Exception when extension module is not found."""Errors specific to slash commands and application commands.
class ApplicationCommandError(DiscordException):
"""Base exception for application command errors."""
class ApplicationCommandInvokeError(ApplicationCommandError):
"""Exception wrapping errors during application command execution."""
def __init__(self, e: Exception):
"""
Initialize application command invoke error.
Parameters:
- e: Original exception that was raised
"""
original: Exception
class ApplicationCommandOnCooldown(ApplicationCommandError):
"""Exception when application command is on cooldown."""
def __init__(self, cooldown: Cooldown, retry_after: float, type: BucketType):
"""
Initialize application command cooldown exception.
Parameters:
- cooldown: Cooldown configuration
- retry_after: Time until command can be used again
- type: Cooldown bucket type
"""
cooldown: Cooldown
retry_after: float
type: BucketType
class ApplicationCommandCheckFailure(ApplicationCommandError):
"""Exception when application command check fails."""Event system for handling and responding to various error conditions.
@bot.event
async def on_error(event: str, *args, **kwargs):
"""
Called when an error occurs in event handlers.
Parameters:
- event: Event name that caused the error
- args: Event arguments
- kwargs: Event keyword arguments
"""
@bot.event
async def on_command_error(ctx: Context, error: CommandError):
"""
Called when command execution raises an error.
Parameters:
- ctx: Command context
- error: Exception that was raised
"""
@bot.event
async def on_application_command_error(inter: ApplicationCommandInteraction, error: Exception):
"""
Called when application command execution raises an error.
Parameters:
- inter: Application command interaction
- error: Exception that was raised
"""
async def on_slash_command_error(inter: ApplicationCommandInteraction, error: Exception):
"""
Called when slash command execution raises an error.
Parameters:
- inter: Slash command interaction
- error: Exception that was raised
"""
async def on_user_command_error(inter: ApplicationCommandInteraction, error: Exception):
"""
Called when user command execution raises an error.
Parameters:
- inter: User command interaction
- error: Exception that was raised
"""
async def on_message_command_error(inter: ApplicationCommandInteraction, error: Exception):
"""
Called when message command execution raises an error.
Parameters:
- inter: Message command interaction
- error: Exception that was raised
"""Utilities for error recovery, logging, and debugging assistance.
class ErrorHandler:
"""Error handling and recovery utilities."""
def __init__(self, bot: Bot):
self.bot = bot
self.error_log = []
self.error_counts = defaultdict(int)
def log_error(self, error: Exception, context: str = None):
"""
Log error with context information.
Parameters:
- error: Exception to log
- context: Additional context information
"""
def get_error_embed(self, error: Exception, ctx: Context = None) -> Embed:
"""
Create error embed for user display.
Parameters:
- error: Exception to format
- ctx: Command context if available
Returns:
Formatted error embed
"""
async def handle_http_error(self, error: HTTPException, ctx: Context = None) -> bool:
"""
Handle HTTP errors with appropriate user feedback.
Parameters:
- error: HTTP exception
- ctx: Command context if available
Returns:
True if error was handled
"""
async def handle_permission_error(self, error: CheckFailure, ctx: Context) -> bool:
"""
Handle permission-related errors.
Parameters:
- error: Permission check failure
- ctx: Command context
Returns:
True if error was handled
"""
async def handle_cooldown_error(self, error: CommandOnCooldown, ctx: Context) -> bool:
"""
Handle cooldown errors with retry information.
Parameters:
- error: Cooldown exception
- ctx: Command context
Returns:
True if error was handled
"""
async def handle_argument_error(self, error: UserInputError, ctx: Context) -> bool:
"""
Handle argument parsing and conversion errors.
Parameters:
- error: User input error
- ctx: Command context
Returns:
True if error was handled
"""
def format_traceback(error: Exception) -> str:
"""
Format exception traceback for logging.
Parameters:
- error: Exception to format
Returns:
Formatted traceback string
"""
def get_error_cause(error: Exception) -> str:
"""
Get human-readable error cause.
Parameters:
- error: Exception to analyze
Returns:
Error cause description
"""
async def safe_send(channel: Messageable, content: str = None, **kwargs) -> Optional[Message]:
"""
Safely send message with error handling.
Parameters:
- channel: Channel to send to
- content: Message content
- kwargs: Additional send parameters
Returns:
Sent message if successful, None otherwise
"""
def retry_on_error(*exceptions: Type[Exception], max_retries: int = 3, delay: float = 1.0):
"""
Decorator for retrying operations on specific errors.
Parameters:
- exceptions: Exception types to retry on
- max_retries: Maximum number of retry attempts
- delay: Delay between retries in seconds
Returns:
Decorated function with retry logic
"""
async def with_timeout(coro: Awaitable, timeout: float, default: Any = None) -> Any:
"""
Execute coroutine with timeout protection.
Parameters:
- coro: Coroutine to execute
- timeout: Timeout in seconds
- default: Default value on timeout
Returns:
Coroutine result or default value
"""import disnake
from disnake.ext import commands
import traceback
import logging
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('discord_bot')
bot = commands.Bot(command_prefix='!', intents=disnake.Intents.all())
@bot.event
async def on_ready():
print(f'Bot ready: {bot.user}')
@bot.event
async def on_error(event, *args, **kwargs):
"""Handle errors in event handlers."""
logger.error(f'Error in event {event}:', exc_info=True)
# Log to error channel if available
error_channel = bot.get_channel(ERROR_CHANNEL_ID) # Replace with actual channel ID
if error_channel:
embed = disnake.Embed(
title=f"Error in {event}",
description=f"```py\n{traceback.format_exc()[:1900]}\n```",
color=0xff0000,
timestamp=disnake.utils.utcnow()
)
try:
await error_channel.send(embed=embed)
except:
pass # Avoid error loops
@bot.event
async def on_command_error(ctx, error):
"""Comprehensive command error handling."""
# Ignore these errors
if isinstance(error, (commands.CommandNotFound, commands.DisabledCommand)):
return
# Extract original error from CommandInvokeError
if isinstance(error, commands.CommandInvokeError):
error = error.original
# User input errors
elif isinstance(error, commands.MissingRequiredArgument):
embed = disnake.Embed(
title="Missing Argument",
description=f"You're missing the `{error.param.name}` parameter.",
color=0xff9900
)
embed.add_field(name="Usage", value=f"`{ctx.prefix}{ctx.command.qualified_name} {ctx.command.signature}`")
await ctx.send(embed=embed)
return
elif isinstance(error, commands.BadArgument):
embed = disnake.Embed(
title="Invalid Argument",
description=f"Invalid argument provided: {error}",
color=0xff9900
)
embed.add_field(name="Usage", value=f"`{ctx.prefix}{ctx.command.qualified_name} {ctx.command.signature}`")
await ctx.send(embed=embed)
return
elif isinstance(error, commands.TooManyArguments):
embed = disnake.Embed(
title="Too Many Arguments",
description="You provided too many arguments for this command.",
color=0xff9900
)
embed.add_field(name="Usage", value=f"`{ctx.prefix}{ctx.command.qualified_name} {ctx.command.signature}`")
await ctx.send(embed=embed)
return
# Permission errors
elif isinstance(error, commands.MissingPermissions):
missing = ', '.join(error.missing_permissions)
embed = disnake.Embed(
title="Missing Permissions",
description=f"You need the following permissions: **{missing}**",
color=0xff0000
)
await ctx.send(embed=embed)
return
elif isinstance(error, commands.BotMissingPermissions):
missing = ', '.join(error.missing_permissions)
embed = disnake.Embed(
title="Bot Missing Permissions",
description=f"I need the following permissions: **{missing}**",
color=0xff0000
)
await ctx.send(embed=embed)
return
elif isinstance(error, commands.MissingRole):
embed = disnake.Embed(
title="Missing Role",
description=f"You need the **{error.missing_role}** role to use this command.",
color=0xff0000
)
await ctx.send(embed=embed)
return
elif isinstance(error, commands.MissingAnyRole):
roles = ', '.join(str(role) for role in error.missing_roles)
embed = disnake.Embed(
title="Missing Role",
description=f"You need one of these roles: **{roles}**",
color=0xff0000
)
await ctx.send(embed=embed)
return
elif isinstance(error, commands.NotOwner):
embed = disnake.Embed(
title="Owner Only",
description="Only the bot owner can use this command.",
color=0xff0000
)
await ctx.send(embed=embed)
return
elif isinstance(error, commands.NoPrivateMessage):
embed = disnake.Embed(
title="Guild Only",
description="This command cannot be used in private messages.",
color=0xff0000
)
await ctx.send(embed=embed)
return
elif isinstance(error, commands.PrivateMessageOnly):
embed = disnake.Embed(
title="DM Only",
description="This command can only be used in private messages.",
color=0xff0000
)
await ctx.send(embed=embed)
return
elif isinstance(error, commands.NSFWChannelRequired):
embed = disnake.Embed(
title="NSFW Channel Required",
description="This command can only be used in NSFW channels.",
color=0xff0000
)
await ctx.send(embed=embed)
return
# Cooldown errors
elif isinstance(error, commands.CommandOnCooldown):
embed = disnake.Embed(
title="Command on Cooldown",
description=f"Try again in **{error.retry_after:.2f}** seconds.",
color=0xff9900
)
await ctx.send(embed=embed, delete_after=error.retry_after)
return
elif isinstance(error, commands.MaxConcurrencyReached):
embed = disnake.Embed(
title="Max Concurrency Reached",
description="This command is already being used by too many people. Try again later.",
color=0xff9900
)
await ctx.send(embed=embed)
return
# HTTP errors
elif isinstance(error, disnake.Forbidden):
embed = disnake.Embed(
title="Forbidden",
description="I don't have permission to perform this action.",
color=0xff0000
)
await ctx.send(embed=embed)
return
elif isinstance(error, disnake.NotFound):
embed = disnake.Embed(
title="Not Found",
description="The requested resource could not be found.",
color=0xff0000
)
await ctx.send(embed=embed)
return
elif isinstance(error, disnake.HTTPException):
embed = disnake.Embed(
title="HTTP Error",
description=f"An HTTP error occurred: {error.text}",
color=0xff0000
)
await ctx.send(embed=embed)
return
# Unknown error
else:
embed = disnake.Embed(
title="Unexpected Error",
description="An unexpected error occurred. The developers have been notified.",
color=0xff0000
)
await ctx.send(embed=embed)
# Log the error
logger.error(f'Unexpected error in command {ctx.command}:', exc_info=error)
# Send to error channel
error_channel = bot.get_channel(ERROR_CHANNEL_ID)
if error_channel:
error_embed = disnake.Embed(
title=f"Command Error: {ctx.command.qualified_name}",
color=0xff0000,
timestamp=disnake.utils.utcnow()
)
error_embed.add_field(name="User", value=f"{ctx.author} ({ctx.author.id})", inline=True)
error_embed.add_field(name="Guild", value=f"{ctx.guild} ({ctx.guild.id})" if ctx.guild else "DM", inline=True)
error_embed.add_field(name="Channel", value=f"#{ctx.channel} ({ctx.channel.id})", inline=True)
error_embed.add_field(name="Command", value=f"`{ctx.message.content}`", inline=False)
error_embed.add_field(
name="Error",
value=f"```py\n{traceback.format_exception(type(error), error, error.__traceback__)[-1][:1000]}\n```",
inline=False
)
try:
await error_channel.send(embed=error_embed)
except:
pass
bot.run('YOUR_BOT_TOKEN')@bot.event
async def on_application_command_error(inter, error):
"""Handle application command errors."""
# Extract original error
if isinstance(error, disnake.ApplicationCommandInvokeError):
error = error.original
# Create base embed
embed = disnake.Embed(color=0xff0000, timestamp=disnake.utils.utcnow())
# Handle specific errors
if isinstance(error, commands.MissingPermissions):
missing = ', '.join(error.missing_permissions)
embed.title = "Missing Permissions"
embed.description = f"You need: **{missing}**"
elif isinstance(error, commands.BotMissingPermissions):
missing = ', '.join(error.missing_permissions)
embed.title = "Bot Missing Permissions"
embed.description = f"I need: **{missing}**"
elif isinstance(error, commands.MissingRole):
embed.title = "Missing Role"
embed.description = f"You need the **{error.missing_role}** role."
elif isinstance(error, commands.MissingAnyRole):
roles = ', '.join(str(role) for role in error.missing_roles)
embed.title = "Missing Role"
embed.description = f"You need one of: **{roles}**"
elif isinstance(error, commands.NotOwner):
embed.title = "Owner Only"
embed.description = "Only the bot owner can use this command."
elif isinstance(error, commands.NoPrivateMessage):
embed.title = "Guild Only"
embed.description = "This command cannot be used in DMs."
elif isinstance(error, commands.CommandOnCooldown):
embed.title = "Command on Cooldown"
embed.description = f"Try again in **{error.retry_after:.2f}** seconds."
embed.color = 0xff9900
elif isinstance(error, disnake.Forbidden):
embed.title = "Forbidden"
embed.description = "I don't have permission to perform this action."
elif isinstance(error, disnake.NotFound):
embed.title = "Not Found"
embed.description = "The requested resource was not found."
elif isinstance(error, ValueError):
embed.title = "Invalid Input"
embed.description = "Please check your input and try again."
embed.color = 0xff9900
else:
embed.title = "Unexpected Error"
embed.description = "An unexpected error occurred."
# Log unexpected errors
logger.error(f'Unexpected slash command error:', exc_info=error)
# Send error response
try:
if inter.response.is_done():
await inter.followup.send(embed=embed, ephemeral=True)
else:
await inter.response.send_message(embed=embed, ephemeral=True)
except:
# If we can't send the error message, log it
logger.error(f'Failed to send error response for interaction {inter.id}')
# Command-specific error handlers
@bot.slash_command()
async def divide(inter, a: float, b: float):
"""Divide two numbers."""
try:
result = a / b
await inter.response.send_message(f"{a} ÷ {b} = {result}")
except ZeroDivisionError:
embed = disnake.Embed(
title="Math Error",
description="Cannot divide by zero!",
color=0xff0000
)
await inter.response.send_message(embed=embed, ephemeral=True)
@bot.slash_command()
async def risky_operation(inter):
"""Command that might fail."""
try:
# Potentially risky operation
result = await some_api_call()
await inter.response.send_message(f"Result: {result}")
except aiohttp.ClientError as e:
embed = disnake.Embed(
title="Network Error",
description="Failed to connect to external service. Try again later.",
color=0xff9900
)
await inter.response.send_message(embed=embed, ephemeral=True)
logger.warning(f'API call failed: {e}')
except asyncio.TimeoutError:
embed = disnake.Embed(
title="Timeout",
description="The operation timed out. Try again later.",
color=0xff9900
)
await inter.response.send_message(embed=embed, ephemeral=True)
except Exception as e:
embed = disnake.Embed(
title="Error",
description="An unexpected error occurred.",
color=0xff0000
)
await inter.response.send_message(embed=embed, ephemeral=True)
logger.error(f'Unexpected error in risky_operation: {e}', exc_info=True)import asyncio
import functools
from typing import Optional, Callable, Any
from datetime import datetime, timedelta
class ErrorHandler:
"""Advanced error handling and recovery system."""
def __init__(self, bot):
self.bot = bot
self.error_counts = {}
self.last_errors = {}
self.recovery_strategies = {}
def register_recovery_strategy(self, error_type: type, strategy: Callable):
"""Register a recovery strategy for specific error types."""
self.recovery_strategies[error_type] = strategy
async def handle_error(self, error: Exception, context: dict = None) -> bool:
"""
Handle error with recovery strategies.
Returns True if error was handled and recovered from.
"""
error_type = type(error)
error_key = f"{error_type.__name__}:{str(error)}"
# Track error frequency
now = datetime.utcnow()
if error_key not in self.error_counts:
self.error_counts[error_key] = []
self.error_counts[error_key].append(now)
# Clean old entries (last hour)
cutoff = now - timedelta(hours=1)
self.error_counts[error_key] = [
t for t in self.error_counts[error_key] if t > cutoff
]
# Check if error is recurring
recent_count = len(self.error_counts[error_key])
if recent_count > 5:
logger.warning(f'Recurring error detected: {error_key} ({recent_count} times)')
# Try recovery strategy
if error_type in self.recovery_strategies:
try:
return await self.recovery_strategies[error_type](error, context)
except Exception as recovery_error:
logger.error(f'Recovery strategy failed: {recovery_error}')
return False
# Initialize error handler
error_handler = ErrorHandler(bot)
# Recovery strategies
async def recover_from_forbidden(error: disnake.Forbidden, context: dict) -> bool:
"""Recovery strategy for Forbidden errors."""
if context and 'channel' in context:
channel = context['channel']
# Try to send error message to a different channel
if hasattr(channel, 'guild') and channel.guild:
# Find a channel we can send to
for text_channel in channel.guild.text_channels:
try:
perms = text_channel.permissions_for(channel.guild.me)
if perms.send_messages:
await text_channel.send(
f"⚠️ I don't have permission to perform an action in {channel.mention}. "
f"Please check my permissions."
)
return True
except:
continue
return False
async def recover_from_not_found(error: disnake.NotFound, context: dict) -> bool:
"""Recovery strategy for NotFound errors."""
if context and 'interaction' in context:
inter = context['interaction']
# If interaction is not found, it might have expired
try:
if not inter.response.is_done():
await inter.response.send_message(
"This interaction has expired. Please try the command again.",
ephemeral=True
)
return True
except:
pass
return False
async def recover_from_rate_limit(error: disnake.HTTPException, context: dict) -> bool:
"""Recovery strategy for rate limits."""
if error.status == 429: # Rate limited
retry_after = getattr(error, 'retry_after', 1)
logger.info(f'Rate limited, waiting {retry_after} seconds')
await asyncio.sleep(retry_after)
# Try to notify about the delay
if context and 'ctx' in context:
ctx = context['ctx']
try:
await ctx.send(
f"⏳ Rate limited. Retrying in {retry_after} seconds...",
delete_after=retry_after + 5
)
except:
pass
return True
return False
# Register recovery strategies
error_handler.register_recovery_strategy(disnake.Forbidden, recover_from_forbidden)
error_handler.register_recovery_strategy(disnake.NotFound, recover_from_not_found)
error_handler.register_recovery_strategy(disnake.HTTPException, recover_from_rate_limit)
def with_error_recovery(func):
"""Decorator to add error recovery to functions."""
@functools.wraps(func)
async def wrapper(*args, **kwargs):
try:
return await func(*args, **kwargs)
except Exception as e:
context = {
'function': func.__name__,
'args': args,
'kwargs': kwargs
}
# Try to extract useful context
for arg in args:
if isinstance(arg, (commands.Context, disnake.ApplicationCommandInteraction)):
context['ctx'] = arg
if hasattr(arg, 'channel'):
context['channel'] = arg.channel
if hasattr(arg, 'guild'):
context['guild'] = arg.guild
break
# Attempt recovery
recovered = await error_handler.handle_error(e, context)
if not recovered:
# Re-raise if recovery failed
raise
return wrapper
# Retry decorator
def retry_on_error(*error_types, max_retries=3, delay=1.0, backoff=2.0):
"""Decorator to retry function calls on specific errors."""
def decorator(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_retries + 1):
try:
return await func(*args, **kwargs)
except error_types as e:
last_exception = e
if attempt == max_retries:
break
wait_time = delay * (backoff ** attempt)
logger.info(f'Retrying {func.__name__} in {wait_time}s (attempt {attempt + 1}/{max_retries})')
await asyncio.sleep(wait_time)
# All retries failed
raise last_exception
return wrapper
return decorator
# Usage examples
@bot.command()
@with_error_recovery
@retry_on_error(disnake.HTTPException, max_retries=3, delay=2.0)
async def reliable_command(ctx):
"""A command with error recovery and retry logic."""
# This might fail with HTTPException
await ctx.send("This command has error recovery!")
@bot.slash_command()
@with_error_recovery
async def fetch_data(inter, url: str):
"""Fetch data from URL with error handling."""
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
if resp.status == 200:
data = await resp.text()
await inter.response.send_message(f"Data length: {len(data)} characters")
else:
await inter.response.send_message(f"HTTP {resp.status}: {resp.reason}")
except asyncio.TimeoutError:
await inter.response.send_message("⏰ Request timed out", ephemeral=True)
except aiohttp.ClientError as e:
await inter.response.send_message(f"🌐 Network error: {e}", ephemeral=True)
# Safe execution context manager
class SafeExecution:
"""Context manager for safe code execution with error handling."""
def __init__(self, ctx_or_inter, *,
error_message="An error occurred",
log_errors=True,
reraise=False):
self.ctx_or_inter = ctx_or_inter
self.error_message = error_message
self.log_errors = log_errors
self.reraise = reraise
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
if self.log_errors:
logger.error(f'Error in safe execution: {exc_val}', exc_info=exc_val)
# Send error message
try:
if isinstance(self.ctx_or_inter, commands.Context):
await self.ctx_or_inter.send(self.error_message)
else:
if self.ctx_or_inter.response.is_done():
await self.ctx_or_inter.followup.send(self.error_message, ephemeral=True)
else:
await self.ctx_or_inter.response.send_message(self.error_message, ephemeral=True)
except:
pass # Avoid error loops
# Suppress exception unless reraise is True
return not self.reraise
# Usage of SafeExecution
@bot.command()
async def safe_command(ctx):
"""Command using safe execution context manager."""
async with SafeExecution(ctx, error_message="Failed to process your request"):
# Potentially risky operation
result = await some_risky_operation()
await ctx.send(f"Result: {result}")
# Error monitoring and alerts
class ErrorMonitor:
"""Monitor errors and send alerts when thresholds are exceeded."""
def __init__(self, bot, alert_channel_id: int, threshold: int = 10):
self.bot = bot
self.alert_channel_id = alert_channel_id
self.threshold = threshold
self.error_window = timedelta(minutes=15)
self.recent_errors = []
def record_error(self, error: Exception, context: str = None):
"""Record an error occurrence."""
now = datetime.utcnow()
self.recent_errors.append({
'timestamp': now,
'error': str(error),
'type': type(error).__name__,
'context': context
})
# Clean old errors
cutoff = now - self.error_window
self.recent_errors = [e for e in self.recent_errors if e['timestamp'] > cutoff]
# Check if we should send an alert
if len(self.recent_errors) >= self.threshold:
asyncio.create_task(self._send_alert())
async def _send_alert(self):
"""Send error alert to monitoring channel."""
channel = self.bot.get_channel(self.alert_channel_id)
if not channel:
return
error_types = {}
for error in self.recent_errors:
error_type = error['type']
error_types[error_type] = error_types.get(error_type, 0) + 1
embed = disnake.Embed(
title="🚨 Error Alert",
description=f"**{len(self.recent_errors)}** errors in the last 15 minutes",
color=0xff0000,
timestamp=disnake.utils.utcnow()
)
error_summary = '\n'.join([f"**{error_type}**: {count}" for error_type, count in error_types.items()])
embed.add_field(name="Error Types", value=error_summary, inline=False)
try:
await channel.send(embed=embed)
except:
pass
# Clear recent errors after alert
self.recent_errors.clear()
# Initialize error monitor
error_monitor = ErrorMonitor(bot, ERROR_ALERT_CHANNEL_ID)
# Update error handlers to use monitor
@bot.event
async def on_command_error(ctx, error):
"""Enhanced command error handler with monitoring."""
error_monitor.record_error(error, f'Command: {ctx.command}')
# Original error handling code here...
# [Previous error handling implementation]
@bot.event
async def on_application_command_error(inter, error):
"""Enhanced application command error handler with monitoring."""
error_monitor.record_error(error, f'Slash Command: {inter.data.name}')
# Original error handling code here...
# [Previous error handling implementation]Install with Tessl CLI
npx tessl i tessl/pypi-disnake