Wiki
猫Table of Contents
- Error Handling
- Overview
- What happens when an error is returned
- ErrorTemplate(...)
- Returning an error versus replying manually
- Good places to return errors
- Good places to avoid returning errors
- Callback-specific behavior
- Validation and helper errors
- Middleware and errors
- Logging behavior
- Practical patterns
- Recommendations
- Related pages
Error Handling
Russian version: Error-Handling-RU
Laniakea uses error-returning handlers so command, payload, and update-handler failures can flow through one consistent mechanism. This page explains when to return errors, when to answer manually, and how ErrorTemplate(...) affects what users see.
Overview
Handlers in Laniakea return error.
That applies to:
- command handlers;
- payload handlers;
- non-command update handlers registered with
AddUpdateHandler(...).
The basic pattern is:
func ping(ctx *laniakea.MessageContext, db *App) error {
ctx.Answer("pong")
return nil
}
When a handler returns an error, the bot routes it through the context error helper instead of ignoring it.
By default, returned handler errors remain user-visible for backward compatibility. Laniakea also provides explicit markers when you want to distinguish between user-facing and internal-only failures:
AsUserError(err)keeps the error on the user-facing path explicitly;AsInternalError(err)logs the error but suppresses the automatic user reply.
What happens when an error is returned
The context error flow does three things:
- formats the error text using the bot's
errorTemplate; - sends the formatted error back to the user;
- logs the original error with the current logger.
Behavior depends on the update kind:
- for callback queries, Laniakea answers the callback query with notification text;
- for message-driven flows, Laniakea sends a plain text reply.
ErrorTemplate(...)
Use Bot.ErrorTemplate(...) or BotOpts.ErrorTemplate to control the user-facing error format.
Example:
bot.ErrorTemplate("Error\n\n%s")
The %s placeholder is replaced with err.Error().
If no explicit template is set, the bot uses "%s".
Returning an error versus replying manually
There are two valid styles.
Return an error
Use this when:
- the failure should go through centralized formatting;
- you want consistent behavior across handlers;
- the error itself is already the right user-facing message.
if !allowed {
return errors.New("access denied")
}
If you want to make the intent explicit, you can return:
return laniakea.AsUserError(errors.New("access denied"))
Reply manually and return nil
Use this when:
- you want full control over the response shape;
- you want to send a richer or more specific message than a generic error template;
- the situation is not really an exceptional failure.
if !allowed {
ctx.Answer("Access denied")
return nil
}
Return an internal-only error
Use this when:
- the failure should be logged for operators, not shown to the user;
- the error indicates an internal invariant problem or infrastructure issue;
- exposing the raw error text would be noisy or misleading UX.
if err := db.WarmCache(); err != nil {
return laniakea.AsInternalError(fmt.Errorf("warm cache: %w", err))
}
Good places to return errors
Returning an error is a good fit for:
- failed validation that should be presented uniformly;
- database or API failures that should surface to the user;
- command execution failures where the user should see a standard error response.
It is especially useful when many handlers share the same style of failure reporting.
Good places to avoid returning errors
Avoid returning errors when:
- you already produced the desired user-facing response;
- the condition is expected control flow rather than a failure;
- the action is best handled silently or via logging only.
Examples:
- permission gate that intentionally returns no output;
- callback interaction where you already answered with a custom alert;
- middleware-based rejection path handled separately.
Callback-specific behavior
For callback-driven flows, returned errors become callback-query answers rather than regular chat messages.
This matters because callback error UX is different:
- the user sees a callback notification;
- no new chat message is posted for the error path.
If you want a different callback UX, answer manually with:
AnswerCbQuery()AnswerCbQueryText(...)AnswerCbQueryAlert(...)AnswerCbQueryUrl(...)
and return nil.
Validation and helper errors
Some failures never reach your handler because helpers validate earlier.
Examples:
- empty or oversized messages are rejected before Telegram API requests are sent;
- invalid command argument shapes can stop command execution before the handler body runs.
That means not every user-visible failure should be modeled as a returned handler error. Some are enforced by helpers and command plumbing before your business logic starts.
Middleware and errors
Middleware does not return error; it returns bool.
So middleware has a different contract:
truecontinues the chain;falsestops the chain.
If middleware wants to show an error-like response, it should do so explicitly and then return false.
Related page:
Logging behavior
Returned handler errors are logged through the current context logger.
Depending on the route, that may be:
- the plugin logger;
- or the bot logger if the plugin has no dedicated logger.
This is one reason returning errors is useful: it keeps operational visibility aligned with the same handler path that produced the user-facing failure.
Internal-only errors follow the same logging path, but they do not produce the automatic user reply.
Related page:
Practical patterns
Centralized command failure
plugin.NewCommand(func(ctx *laniakea.MessageContext, db *App) error {
result, err := db.DoWork()
if err != nil {
return fmt.Errorf("failed to build report: %w", err)
}
ctx.Answer(result)
return nil
}, "report")
Manual denial response
plugin.NewCommand(func(ctx *laniakea.MessageContext, db *App) error {
if ctx.From == nil || !db.Allowed(ctx.From.ID) {
ctx.Answer("Access denied")
return nil
}
return doProtectedWork(ctx, db)
}, "admin")
Callback-specific manual alert
plugin.NewPayload(func(ctx *laniakea.MessageContext, db *App) error {
if !ready {
ctx.AnswerCbQueryAlert("This action is not available yet")
return nil
}
return nil
}, "start")
Recommendations
- Return
errorwhen you want centralized error formatting and logging. - Wrap with
AsInternalError(...)when the centralized path should log but stay silent for the user. - Reply manually and return
nilwhen the response is part of normal control flow. - Keep user-facing error strings clear and action-oriented.
- Use one consistent style across a plugin when possible.
Related pages
Navigation
Start here
Runtime and Architecture
- Bot-Lifecycle
- Webhook-Runtime
- Middleware
- Runners
- Error-Handling
- Logging
- Update-Routing-Model
- Policies
- Scenes
Interaction and Telegram API
- Inline-Keyboards-and-Payloads
- Auto-Generated-Commands
- Drafts
- Rich-Messages
- Localization
- Rate-Limiting
- tgapi-Overview