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