> ## Documentation Index
> Fetch the complete documentation index at: https://poly-bot.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# PolyBot Compiler Diagnostics: CUR001–CUR056 Guide

> Complete reference for every PolyBot source-generator diagnostic (CUR001–CUR056) — severity, trigger condition, and fix, grouped by feature area.

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

> **CUR001 — Unsupported handler signature (Warning)**
>
> **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`.
>
> ```csharp theme={null}
> // ❌ CUR001 — wrong return type
> [MessageHandler]
> public static string BadHandler(Message msg) => "not supported";
>
> // ✅ correct
> [MessageHandler]
> public static Task<Result> GoodHandler(Message msg)
>     => Task.FromResult(Result.StopRouting);
> ```

***

## Commands

> **CUR002 — Command filter on non-message payload (Warning)**
>
> **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.

> **CUR003 — Arg, Rest or Parse on non-command handler (Warning)**
>
> **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.

> **CUR004 — Non-constant command prefix (Warning)**
>
> **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 = '!'`.

> **CUR005 — Incompatible argument parameter type (Warning)**
>
> **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.

> **CUR006 — Multiple Rest parameters (Warning)**
>
> **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.

> **CUR007 — Rest parameter is not a string (Warning)**
>
> **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`.

> **CUR008 — Parse after Rest (Warning)**
>
> **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]`.

> **CUR009 — Parse parameter is not a string (Warning)**
>
> **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`.

> **CUR020 — Command alias is not a valid BotFather command (Error)**
>
> **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.

> **CUR021 — Command has no BotFather description (Warning)**
>
> **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.

> **CUR022 — Command description exceeds 256 characters (Error)**
>
> **Trigger:** `[Command].Description` is longer than 256 characters. BotFather rejects descriptions that exceed this limit.
>
> **Fix:** Shorten the description to at most 256 characters.

> **CUR023 — Duplicate command in the same scope and language (Warning)**
>
> **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.

> **CUR052 — Command alias starts with the prefix (Warning)**
>
> **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

> **CUR010 — State type argument must be an enum (Error)**
>
> **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)]`.

> **CUR011 — Mutually exclusive state conditions (Warning)**
>
> **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

> **CUR012 — Generic filter class (Warning)**
>
> **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

> **CUR013 — Update awaiter without an await site (Warning)**
>
> **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.

> **CUR014 — TextMatches on a non-Message await target (Warning)**
>
> **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.

> **CUR015 — Unknown filter in await chain (Warning)**
>
> **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.

> **CUR016 — Unsupported Where predicate (Warning)**
>
> **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).
>
> ```csharp theme={null}
> // ❌ CUR016 — captures variable
> .Where(m => m.Text == expectedText)
>
> // ✅ correct
> .Where(static m => m.Text != null && m.Text.Length > 0)
> ```

***

## Patterns (callback-data routing)

> **CUR024 — Callback pattern can never fit callback\_data (Error)**
>
> **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.

> **CUR025 — Callback pattern placeholders can never fit callback\_data (Error)**
>
> **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.

> **CUR026 — Callback pattern placeholder has no \[Arg] parameter (Error)**
>
> **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.

> **CUR027 — \[Arg] parameter is not captured by the callback pattern (Error)**
>
> **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.

> **CUR028 — \[Arg] type has no accessible TryParse (Error)**
>
> **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.

> **CUR029 — \[Pattern] requires a CallbackQuery handler (Error)**
>
> **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]`.

> **CUR030 — Callback pattern constraint does not match parameter type (Error)**
>
> **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.

> **CUR031 — Malformed callback pattern (Error)**
>
> **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

> **CUR032 — Exception handler parameter cannot be resolved in the error path (Error)**
>
> **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]`).

> **CUR033 — Only one \[ExceptionHandler] is allowed (Error)**
>
> **Trigger:** More than one method is decorated with `[ExceptionHandler]`. Only the first declaration is used.
>
> **Fix:** Remove `[ExceptionHandler]` from all but one method.

> **CUR034 — Exception handler must return Task or ValueTask (Error)**
>
> **Trigger:** An `[ExceptionHandler]` method does not return `Task` or `ValueTask`. The attribute is ignored.
>
> **Fix:** Change the return type to `Task` or `ValueTask`.

***

## Keyboards

> **CUR017 — Unknown keyboard placeholder (Warning)**
>
> **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.

> **CUR018 — Generated keyboard must return InlineKeyboardMarkup (Error)**
>
> **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`.

> **CUR019 — Generated keyboard member must be partial (Warning)**
>
> **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.

> **CUR050 — callback\_data literal is invalid (Error)**
>
> **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.
>
> ```csharp theme={null}
> // ❌ CUR050 — 65 bytes
> InlineKeyboardButton.WithCallbackData("pick", new string('a', 65));
>
> // ✅ correct
> InlineKeyboardButton.WithCallbackData("pick", "item:42");
> ```

> **CUR055 — callback\_data built by string interpolation (Info)**
>
> **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:
>
> ```csharp theme={null}
> // ⚠️ CUR055 — ad-hoc format
> InlineKeyboardButton.WithCallbackData("Item 42", $"item:{itemId}");
>
> // ✅ typed pattern routing
> [CallbackQueryHandler, Pattern("item:{id:int}")]
> public static Task<Result> OnItem(CallbackQuery query, [Arg] int id) { ... }
> ```

***

## Guest messages

> **CUR046 — GuestMessage handler posts to a chat (Warning)**
>
> **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:
>
> ```csharp theme={null}
> // ❌ CUR046
> await bot.SendMessage(msg.Chat, "hello");
> await bot.SendPhoto(msg.Chat, photo);
>
> // ✅ correct
> await bot.AnswerGuestQuery(msg.GuestQueryId!, result);
> ```

> **CUR047 — GuestMessage handler can answer the guest query more than once (Warning)**
>
> **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

> **CUR048 — CallbackQuery handler can answer the callback query more than once (Warning)**
>
> **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.

> **CUR056 — CallbackQuery handler never answers the query (Info)**
>
> **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:
>
> ```csharp theme={null}
> [CallbackQueryHandler]
> public static async Task<Result> OnCallback(CallbackQuery query, ITelegramBotClient client)
> {
>     // ... handle the callback ...
>     await client.AnswerCallbackQuery(query.Id); // ✅ dismiss the loading state
>     return Result.StopRouting;
> }
> ```
>
> Silent data-collection callbacks can ignore this Info.

***

## Inline queries

> **CUR049 — InlineQuery handler can answer the inline query more than once (Warning)**
>
> **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.

> **CUR051 — AnswerInlineQuery result list exceeds 50 items (Error)**
>
> **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.
>
> ```csharp theme={null}
> // ❌ CUR051 — 51 items
> InlineQueryResult[] results = [r01, r02, /* ... */ r51];
> await client.AnswerInlineQuery(query.Id, results);
>
> // ✅ correct — 50 items or fewer
> ```

***

## Async hygiene

> **CUR053 — Handler blocks on asynchronous work (Info)**
>
> **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.

> **CUR054 — Handler creates its own TelegramBotClient (Info)**
>
> **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:
>
> ```csharp theme={null}
> // ⚠️ CUR054
> [MessageHandler]
> public static Task<Result> OnMessage(Message msg)
> {
>     var client = new TelegramBotClient("token");
>     ...
> }
>
> // ✅ correct
> [MessageHandler]
> public static Task<Result> OnMessage(Message msg, ITelegramBotClient client) { ... }
> ```

***

## Rate limiting

> **CUR035 — Invalid \[Throttled] configuration (Warning)**
>
> **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

> **CUR036 — Handlers are not routed in library projects (Warning)**
>
> **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](https://poly-bot.mintlify.site/advanced/library-vs-executable.md) for details.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.