Skip to main content
The PolyBot source generator reports diagnostics as standard Roslyn compiler messages. Warnings do not prevent compilation but indicate that an attribute or parameter was silently skipped. Errors prevent the affected handler, keyboard, or pattern from being emitted and must be resolved before the router will compile. All diagnostics use the CUR prefix and appear in the Error List / build output like any other C# diagnostic.

Handlers & general

Trigger: A method decorated with a [XxxHandler] attribute returns something other than Task<Result>, ValueTask<Result>, Task, or ValueTask.Fix: Change the return type to one of the four supported types. Task/ValueTask (non-generic) stop routing after the handler completes; Task<Result>/ValueTask<Result> let you control routing with Result.StopRouting / Result.ContinueRouting.

Commands

Trigger: [Command] is applied to a handler whose update payload is not Message (e.g. [CallbackQueryHandler, Command(...)]).Fix: Remove [Command] from non-message handlers. Commands only make sense for Message payloads. The [Command] attribute is ignored and the handler runs without a command guard.
Trigger: A parameter on a handler that has no [Command] attribute (or has [Command] without Aliases) is annotated with [Arg], [Rest], or [Parse]. The parameter is treated as a normal DI service.Fix: Add [Command(Aliases = [...])] to the handler, or remove the [Arg]/[Rest]/[Parse] annotation and inject the value from DI instead.
Trigger: The Prefix named argument on [Command] is not a constant char literal. The generator falls back to '/'.Fix: Use a constant character literal: Prefix = '!'.
Trigger: A parameter annotated with [Arg] has a type that is not T, Nullable<T>, or object. The parameter is treated as a DI dependency.Fix: Change the parameter type to the argument type, T?, or object. Primitive types (int, long, Guid, enums, and anything with an accessible TryParse) are supported.
Trigger: A handler declares more than one [Rest] parameter. Extra [Rest] parameters are treated as DI dependencies, which will fail at runtime because string is not registered.Fix: Keep only one [Rest] parameter per handler.
Trigger: [Rest] is applied to a parameter whose type is not string. The parameter is treated as a DI dependency.Fix: Change the parameter type to string.
Trigger: A [Parse] parameter is declared after a [Rest] parameter. Because [Rest] consumes all remaining text, nothing is left for [Parse]. The parameter is treated as a DI dependency.Fix: Reorder parameters so all [Parse] parameters appear before [Rest].
Trigger: [Parse] is applied to a parameter whose type is not string. The parameter is treated as a DI dependency.Fix: Change the parameter type to string.
Trigger: An alias in [Command(Aliases = [...])] does not match ^[a-z0-9_]{1,32}$ — BotFather’s required format for command names.Fix: Use only lowercase ASCII letters, digits, and underscores; keep the name between 1 and 32 characters.
Trigger: A [Command] with Aliases has no Description set and IsHidden is false. The command cannot be synchronized with BotFather.Fix: Add Description = "Short description" to [Command], or set IsHidden = true if you intentionally do not want the command in the menu.
Trigger: [Command].Description is longer than 256 characters. BotFather rejects descriptions that exceed this limit.Fix: Shorten the description to at most 256 characters.
Trigger: Two or more handlers declare the same alias for the same CommandScope and LanguageCode combination. Only the first is sent to BotFather.Fix: Deduplicate the alias across the affected handlers, or assign different Scope/LanguageCode values to each so they target different BotFather menus.
Trigger: An alias in [Command(Aliases = [...])] starts with the same character as the command Prefix (e.g. Aliases = ["/start"] with the default / prefix). The matcher looks for the prefix followed by the alias, so //start can never match — the command is dead.Fix: Drop the leading prefix from the alias: Aliases = ["start"]. This check does not apply to whitespace prefixes (BotFather-style mention commands).

State

Trigger: The type argument TEnum of [State<TEnum>] or [NoState<TEnum>] is not an enum type. The attribute is ignored.Fix: Use an enum type as the type argument: [State<RegistrationStep>(RegistrationStep.AwaitingAge)].
Trigger: A handler declares both [State<TEnum>(value)] and [NoState<TEnum>] for the same (TEnum, StateKey) pair. The conditions are AND-ed, so the handler never matches.Fix: Remove one of the conflicting attributes.

Filters

Trigger: A class implementing IUpdateFilter has one or more generic type parameters. The generator cannot emit a wrapper attribute for a generic class; the filter is skipped.Fix: Make the filter class non-generic. If you need parameterized behavior, use constructor injection instead.

Awaits

Trigger: A handler takes an IUpdateAwaiter parameter but the generator found no WaitFor*Async(...) call on it in the method body. If the handler were to call WaitFor*Async at runtime without the corresponding branch in the router, the polling loop would deadlock.Fix: Call WaitFor*Async directly on the IUpdateAwaiter parameter (not through a wrapper method — the generator does not follow indirection by design). If you do not intend to suspend the handler, remove the IUpdateAwaiter parameter.
Trigger: .TextMatches(...) is chained on a WaitFor*Async call whose target DTO is not a message-shaped payload (Message). The text check can never match; no implicit await branch is generated.Fix: Remove .TextMatches(...) from the chain, or change the awaited type to a message-shaped DTO.
Trigger: A .With*() call in an await chain references a filter name that could not be resolved to a known IUpdateFilter class. The filter check is not emitted into the await branch.Fix: Ensure the filter class exists in the current compilation (or a referenced library), is concrete (non-abstract), non-generic, and implements IUpdateFilter. Check for typos in the With* extension name.
Trigger: A .Where(...) predicate in an await chain does not meet the generator’s static-lambda constraints. The predicate must be static, non-async, expression-bodied, and reference only the parameter and literals. The predicate is not emitted into the await branch.Fix: Add the static modifier, remove any async modifier, convert to an expression body, and ensure the body references only the lambda parameter (no statics, no captured variables).

Patterns (callback-data routing)

Trigger: The literal skeleton of a [Pattern] template is already longer than Telegram’s 64-byte callback_data limit. No update can ever match the template.Fix: Shorten the literal segments of the template so the skeleton fits within 64 bytes.
Trigger: The literal skeleton plus the minimum non-empty size of each placeholder (1 byte each) already exceeds 64 bytes. No valid data can ever match.Fix: Shorten the template or reduce the number of non-wildcard placeholders.
Trigger: A {name} placeholder in a [Pattern] template has no matching [Arg]-annotated parameter on the handler.Fix: Add [Arg] T name to the handler parameter list, or remove the {name} placeholder from the template.
Trigger: A handler has an [Arg]-annotated parameter but the [Pattern] template contains no matching {name} placeholder.Fix: Add a {paramName} or {paramName:constraint} placeholder to the template, or remove the [Arg] annotation and let the parameter be resolved from DI.
Trigger: The type of an [Arg] parameter used in a [Pattern] has no accessible static TryParse(string, out T) or TryParse(ReadOnlySpan<char>, out T) method. The parameter binds to default(T) in the generated code.Fix: Use a type that has an accessible TryParse, or add one to a custom type. BCL numerics, bool, Guid, DateTime, and all enums are automatically supported.
Trigger: [Pattern] is applied to a handler that is not a [CallbackQueryHandler]. The pattern is ignored.Fix: Remove [Pattern] from the handler, or change the handler attribute to [CallbackQueryHandler].
Trigger: A placeholder constraint (e.g. {id:int}) does not match the C# type of the corresponding [Arg] parameter (e.g. the parameter is string), or the constraint name is unknown.Fix: Align the constraint with the parameter type, or remove the constraint to accept any string segment.
Trigger: The [Pattern] template string is structurally invalid — e.g. unbalanced braces, empty segments, a wildcard {*name} that is not the last segment, or an empty placeholder name.Fix: Correct the template syntax. Examples of valid forms: "item:{id}", "cart:{id:int}:checkout", "logs:{*path}".

Exception handling

Trigger: A parameter of an [ExceptionHandler] method is update-bound (e.g. typed as Message, CallbackQuery) or uses [Key] without a constant key. There is no current update in the error path, so these parameters cannot be resolved.Fix: Remove update-bound parameters from the exception handler. Allowed parameter types are Exception, HandleErrorSource, ITelegramBotClient, CancellationToken, and DI services (optionally keyed with a constant [Key]).
Trigger: More than one method is decorated with [ExceptionHandler]. Only the first declaration is used.Fix: Remove [ExceptionHandler] from all but one method.
Trigger: An [ExceptionHandler] method does not return Task or ValueTask. The attribute is ignored.Fix: Change the return type to Task or ValueTask.

Keyboards

Trigger: A {parameter} placeholder in [CallbackButton].CallbackData does not match any parameter of the declaring partial method. The generated implementation will not compile until the mismatch is resolved.Fix: Either add a parameter with the matching name to the method, or remove or rename the placeholder.
Trigger: A method or property annotated with [CallbackButton] returns a type other than InlineKeyboardMarkup. The generator only implements InlineKeyboardMarkup returns.Fix: Change the return type to Telegram.Bot.Types.ReplyMarkups.InlineKeyboardMarkup.
Trigger: A method or property annotated with [CallbackButton] is not marked partial (or its containing type is not partial). The generator cannot provide the implementation.Fix: Add partial to the method (or property in C# 13) and to every enclosing type.
Trigger: A constant callback_data string passed to InlineKeyboardButton.WithCallbackData(...), InlineKeyboardBuilder.CallbackButton(...), or an InlineKeyboardButton object initializer is empty or longer than 64 bytes. The Bot API requires 1–64 bytes, so the button fails at runtime.Fix: Keep callback data between 1 and 64 UTF-8 bytes. Note that the limit is in bytes, not characters — non-ASCII data fills the budget faster.
Trigger: A callback_data argument (same call sites as CUR050) is an interpolated string ($"item:{id}"). Interpolated data bypasses compile-time length checks, is parsed by hand at the receiving handler, and drifts easily.Fix: Consider a generated callback keyboard with [CallbackButton] placeholders, or [Pattern] with CallbackPatternMatcher for typed, length-checked parsing:

Guest messages

Trigger: A [GuestMessageHandler] calls a chat-bound send method (SendMessage, SendPhoto, SendVideo, SendAudio, SendDocument, SendSticker, SendDice, SendPoll, SendLocation, SendVenue, SendContact, SendPaidMedia, and the rest of the Send* family). Guest messages are not addressed to a bot chat — replies must go through AnswerGuestQuery with the message’s GuestQueryId. A regular send targets the wrong chat or fails silently.Fix: Answer with AnswerGuestQuery instead:
Trigger: Control-flow analysis of the handler body found an execution path on which AnswerGuestQuery can run twice for one update — sequential calls, two calls in one loop body, or a call before and another inside a loop. A guest query can be answered only once; every call after the first fails.Not reported: two calls on mutually exclusive if/else branches (only one runs per update), or a single call inside a loop whose iterations may await new guest messages. Local functions fall back to straight-line counting.Fix: Keep a single AnswerGuestQuery call per update, or move the decision into one answer call.

Callback queries

Trigger: Control-flow analysis of the handler body found an execution path on which AnswerCallbackQuery can run twice for one update — sequential calls, two calls in one loop body, or a call before and another inside a loop. A callback query can be answered only once; every call after the first fails.Not reported: two calls on mutually exclusive if/else branches (only one runs per update). Local functions fall back to straight-line counting; lambda bodies are analyzed via their own control-flow graph.Fix: Keep a single AnswerCallbackQuery call per update, or move the decision into one answer call.
Trigger: A [CallbackQueryHandler] has no reachable AnswerCallbackQuery call at all. The button on the client’s side keeps showing a loading indicator until the Bot API times it out.Fix: If the user should get feedback (button un-press, toast notification), call AnswerCallbackQuery — even with no text:
Silent data-collection callbacks can ignore this Info.

Inline queries

Trigger: Control-flow analysis of the handler body found an execution path on which AnswerInlineQuery can run twice for one update. An inline query can be answered only once; every call after the first fails.Not reported: two calls on mutually exclusive if/else branches (only one runs per update). Local functions fall back to straight-line counting; lambda bodies are analyzed via their own control-flow graph.Fix: Keep a single AnswerInlineQuery call per update.
Trigger: An AnswerInlineQuery call passes more than 50 inline results inline — as an array, collection expression, or a local variable initialized with one. The Bot API rejects more than 50 results, so the call fails at runtime.Fix: Cap the result list at 50 items. Collections built with spread elements ([..a, ..b]) or other non-literal expressions are not counted.

Async hygiene

Trigger: A handler accesses .Result, calls .Wait(), or .GetAwaiter().GetResult() on a task-like value. Blocking synchronously stalls the sequential update loop — one slow handler delays every pending update.Fix: Make the handler async and await the operation instead.
Trigger: A handler body constructs new TelegramBotClient(...). A per-call client bypasses the shared, configured HttpClient (proxy, retry, logging, rate-limit handlers) registered by AddTelegramBot, and breaks PolyTests test doubles.Fix: Inject the shared client through the handler parameter list:

Rate limiting

Trigger: A [Throttled] attribute has an invalid or non-constant value for Limit, PeriodMilliseconds, Scope, or Action. The affected value is clamped to its default (Limit=1, PeriodMilliseconds=1000, Scope=User, Action=Ignore).Fix: Use positive integer literals for Limit and PeriodMilliseconds, and enum literals for Scope and Action.

Library projects

Trigger: A handler method (decorated with [XxxHandler]) is declared in a project with OutputType=Library. Library projects do not get a generated router, so the handler never runs.Fix: Move the handler to an executable project (OutputType=Exe or WinExe). Libraries may only contribute filters, keyboards, and With* extensions. See Library vs Executable for details.