Anchor генерує структуровані повідомлення про помилки, які можна надійно парсити на боці клієнта та використовувати для точної діагностики в локальному середовищі. Цей матеріал описує формат AnchorError, механізм кастомних кодів, перехоплення помилок у клієнтському коді та налаштування логування в local validator.

Формат AnchorError: код, ім'я, повідомлення

Коли інструкція Anchor-програми завершується помилкою, у логах транзакції з'являється типізований рядок. Його структура фіксована і містить три ключові поля:

  • Error Code — ім'я варіанту помилки (наприклад, ConstraintSeeds, AccountNotInitialized).
  • Error Number — числовий ідентифікатор, який передається як custom program error у саму транзакцію.
  • Error Message — людяний опис причини.

Типовий вигляд у логах:

Program log: AnchorError thrown in initialize. Error Code: ConstraintSeeds. Error Number: 2006. Error Message: A seeds constraint was violated.

Після цього рядка йде фінальний статус транзакції:

Program <program_id> failed: custom program error: 0x7d6

Десяткове значення 2006 відповідає шістнадцятковому 0x7d6. Це дозволяє зіставити числовий код із конкретним варіантом помилки без аналізу текстових логів — корисно, коли RPC-провайдер повертає лише метадані транзакції без повних логів.

Важливо розуміти: AnchorError — це рівень абстракції поверх Solana-транзакції. Під ним завжди лежить InstructionError із підтипом Custom (код 0x1). Якщо ви бачите інший підтип InstructionError — це вже не AnchorError, і діагностика відрізняється. Цей випадок розглядається в окремому матеріалі про діагностику InstructionError.

Кастомні error коди в Anchor

Anchor дозволяє визначати власні помилки через атрибут #[error_code] на enum:

#[error_code]
pub enum MyProgramError {
  #[msg("Insufficient balance for this operation")]
  InsufficientBalance,
  #[msg("Withdrawal period has not started yet")]
  WithdrawalNotStarted,
}

Кожен варіант отримує числовий код, що починається з 6000 і зростає на одиницю для кожного наступного варіанту. У цьому прикладі InsufficientBalance отримає код 6000, а WithdrawalNotStarted — 6001.

Щоб викликати таку помилку всередині інструкції, використовується макрос require! або явне повернення Err():

require!(vault.amount >= withdraw_amount, MyProgramError::InsufficientBalance);

У логах це з'явиться як:

Program log: AnchorError thrown in withdraw. Error Code: InsufficientBalance. Error Number: 6000. Error Message: Insufficient balance for this operation.

Обмеження та нюанси

  • Кількість варіантів. Числовий код помилки передається у 32-бітному полі custom program error, тому практична межа значно перевищує потреби будь-якої програми. Проте великий enum ускладнює підтримку та читання логів.
  • Порядок варіантів. Код прив'язаний до позиції варіанту в enum. Якщо ви вставите новий варіант посередині або переставите існуючі — числові коди зсунуться. Це критично, якщо клієнтський код покладається на числові значення замість імен. Рекомендація: додавати нові варіанти лише в кінець enum.
  • Зміна повідомлень. Текст у #[msg(...)] можна змінювати між деплоями без зміни коду помилки. Це безпечно для клієнтів, які парсять за кодом або іменем.
  • Перевірка відповідності. Після оновлення програми переконайтеся, що клієнтська бібліотека використовує актуальний IDL. Розбіжність між IDL і deployed-бінарником призведе до того, що клієнт не зможе розпізнати нові коди помилок.

Перехоплення помилок на боці клієнта

Бібліотека @coral-xyz/anchor надає клас AnchorError, який автоматично парсить логи транзакції та витягує структуровані дані. Базовий патерн обробки:

import { AnchorError } from '@coral-xyz/anchor';

try {
  await program.methods.withdraw(amount).accounts({...}).rpc();
} catch (err) {
  if (err instanceof AnchorError) {
    console.log(err.error.errorCode.code);  // "InsufficientBalance"
    console.log(err.error.errorCode.number); // 6000
    console.log(err.error.errorMessage);  // "Insufficient balance..."
  } else {
    // Не AnchorError — можливо, мережева помилка, insufficient funds тощо
  }
}

Коли парсинг не спрацьовує

Існують ситуації, коли AnchorError не створюється навіть якщо програма викинула Anchor-помилку:

  • RPC-провайдер не повертає логи. Деякі публічні RPC-ендпоінти обрізають логи транзакції. Без логів Anchor не може відновити структуру помилки. У такому разі err.logs буде порожнім або відсутнім, і ви отримаєте_generic SendTransactionError.
  • Симуляція без логування. Якщо ви використовуєте simulateTransaction без вказаного replaceRecentBlockhash або з обмеженими флагами, логи можуть бути неповними.
  • Помилка не від Anchor-програми. Якщо помилка сталася в програмі, написаній без Anchor, або на рівні runtime (наприклад, preflight failure), клас AnchorError не застосовується.

Ручний парсинг як запасний варіант

Якщо автоматичний парсинг недоступний, можна витягти дані з логів за регулярним виразом. Формат рядка стабільний між версиями Anchor:

const log = "AnchorError thrown in withdraw. Error Code: InsufficientBalance. Error Number: 6000. Error Message: Insufficient balance for this operation.";
const match = log.match(/Error Code: (\w+). Error Number: (\d+). Error Message: (.+)/);
// match[1] — код, match[2] — число, match[3] — повідомлення

Цей підхід менш надійний (залежить від стабільності текстового формату), але працює навіть без повноцінного парсера Anchor.

Дебаггінг з Anchor logs у local validator

Локальний валідатор (solana-test-validator) дає повний доступ до логів програми, що суттєво полегшує діагностику порівняно з production-середовищем.

Базовий запуск з логами

За замовчуванням solana-test-validator виводить Program log-рядки у stdout. Щоб побачити їх, достатньо запустити валідатор у терміналі та виконати транзакцію:

solana-test-validator --bpf-program <PROGRAM_ID> target/deploy/your_program.so

У stdout з'являться всі рядки, згенеровані макросом msg! у вашому Rust-коді, а також автоматичні AnchorError-рядки.

Рівні логування через RUST_LOG

Anchor використовує crate log разом із env_logger. Щоб отримати детальніші логи від самого фреймворку (внутрішні перевірки, CPI-виклики, обробку акаунтів), встановіть змінну середовища перед запуском валідатора:

RUST_LOG=anchor_lang=trace solana-test-validator --bpf-program <PROGRAM_ID> target/deploy/your_program.so

Доступні рівні: error, warn, info, debug, trace. Рівень trace генерує значний обсяг виводу — використовуйте його лише для вузьких діагностичних сесій, а не для постійної роботи.

Власні логи через msg!

Макрос msg! — основний інструмент для дебаггінгу в Anchor. Його вивід одразу з'являється в логах валідатора:

msg!("Vault balance before withdrawal: {}", vault.amount);
msg!("Requested amount: {}", withdraw_amount);
msg!("Authority: {}", ctx.accounts.authority.key());

Обмеження логування в local validator

  • Обмеження розміру логів транзакції. Solana runtime обмежує загальний розмір логів однієї транзакції (зазвичай до ~100 KB). Якщо ваша програма генерує забагато msg! викликів, транзакція може завершитися помилкою Log exceeded maximum length ще до виконання бізнес-логіки. У production-коді залишайте лише критичні логи.
  • Відсутність CPI-логів з інших програм. Локальний валідатор показує логи вашої програми, але не деталізує внутрішній стан системних програм (SystemProgram, TokenProgram) або сторонніх Anchor-програм, якщо вони не запущені з trace-рівнем логування.
  • Поведінка відрізняється від production. Локальний валідатор не імітує затримки мережі, конкурентні транзакції та стан блокчейну на момент виконання. Якщо помилка відтворюється лише в production, логи local validator допоможуть зрозуміти логіку, але не відтворять умови виникнення. Для цього існує окремий підхід, описаний в матеріалі про відтворення production-помилок.

Перевірка та відкат

Після додавання діагностичних msg! викликів:

  1. Запустіть anchor build та переконайтеся, що бінарник зібрався без попереджень про неиспользовані змінні (виклики msg! споживають аргументи, тому компілятор не повинен скаржитися).
  2. Запустіть валідатор, виконайте проблемну транзакцію та перевірте, що очікувані логи з'явилися в stdout.
  3. Перед деплоєм видаліть або закоментуйте діагностичні msg! виклики, які не несуть виробничої цінності. Залишені дебаг-логи збільшують розмір бінарника та споживають compute units під час виконання.
  4. Якщо ви змінили enum помилок, переконайтеся, що згенерований IDL (target/idl/your_program.json) містить актуальний список error кодів. Розбіжність IDL і бінарника — поширена причина неможливості парсити помилки на клієнті.

Джерела