REPOSITORY / ScuroNeko/Laniakea

Wiki

KNOWLEDGE REPOSITORY
6
Error Handling
ScuroNeko edited this page 2026-07-09 18:13:22 +03:00

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:

  • true continues the chain;
  • false stops 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 error when 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 nil when 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.