Курс СРС SRS14
до ЛР7 рівень 3 → «5»↔ Лабораторна 7↔ Лекція 7⌨️ команди 3
Самостійна робота · ~3 год

Власні типи помилок і трейт std::error::Error: Display, Debug, Box<dyn Error>

🌌 Єдина мова інцидентів — С.І.Д. любить, коли в його домі лад: щоб усі частини системи розуміли інциденти однаково, кожен власний тип помилки має говорити спільною мовою std::error::Error — інакше, застерігає він, журнал інцидентів із серверів «ТехНови» перетвориться на набір несумісних діалектів, і жодна верхньорівнева функція не обробить їх уніфіковано.

📚 ТЕОРЕТИЧНІ ВІДОМОСТІ

Від enum-помилки до повноцінного типу помилки екосистеми

Попередня тема показала, як enum природно моделює кілька варіантів помилки одного модуля (ParseError::UnexpectedEof, ParseError::InvalidUtf8, ParseError::MissingField(String)). Але щоб такий тип був сумісним з рештою екосистеми Rust — щоб його можна було вивести через {}/{:?}, передати в бібліотеки логування, композитно обробляти разом з чужими типами помилок — він має реалізувати трейт std::error::Error. Формально трейт Error вимагає, щоб тип уже реалізовував два інших трейти: Debug (зазвичай через #[derive(Debug)] — механічне, «діагностичне» представлення для розробника) і Display (реалізується вручну через impl fmt::Display — людяне повідомлення для кінцевого користувача чи журналу). Сам Error додає лише один опційний метод — fn source(&self) -> Option<&(dyn Error + 'static)>, що дозволяє вказати «причину нижчого рівня», яка призвела до цієї помилки, формуючи ланцюжок (chain) причинності.

Практична цінність цього трейту розкривається в композиції: функція, що читає файл (io::Error), парсить його вміст (власний ParseError) і звертається до мережі (reqwest::Error чи інша стороння помилка), у реалізмі потребує єдиного типу повернення для всіх трьох різних помилок. Явний enum-«парасолька» (enum AppError { Io(io::Error), Parse(ParseError), Network(...) } з impl From<io::Error> for AppError для кожного джерела) дає найбільше контролю, але вимагає ручної роботи для кожного нового типу помилки. Альтернатива — Box<dyn Error> (чи Box<dyn Error + Send + Sync> для потокобезпечних сценаріїв): типізоване стирання (type erasure), коли функція повертає Result<T, Box<dyn Error>>, і оператор ? автоматично обгортає будь-яку конкретну помилку, що реалізує Error, у цей спільний вказівник — завдяки бланкетній реалізації impl<E: Error + 'static> From<E> for Box<dyn Error> у стандартній бібліотеці.

Компроміс між цими двома підходами — фундаментальний і практичний: власний enum-тип помилки дає викликачу можливість зробити match і відреагувати по-різному на кожну категорію (наприклад, повторити спробу для мережевої помилки, але не для помилки парсингу) — це типово для бібліотечного коду, де користувачі хочуть точного контролю. Box<dyn Error> втрачає цю можливість розрізнення без подальшого приведення типів (downcast_ref), зате мінімізує «бойлерплейт» — типово для прикладного коду (main.rs, скриптів, CLI-утиліт), де достатньо просто вивести повідомлення про помилку й завершити роботу з ненульовим кодом виходу.

У реальних проєктах ручне написання impl Display/impl Error/ impl From<...> для кожного власного типу помилки — рутинна й багатослівна робота, тому спільнота виробила популярні крейти-помічники (crate): thiserror генерує ці реалізації через похідні макроси (#[derive(thiserror::Error)] з атрибутами #[error("...")] на кожному варіанті) для бібліотечного коду, де важлива структура типу помилки; anyhow надає зручний anyhow::Error (концептуально схожий на Box<dyn Error>, але з додатковим контекстом і зручним .context(...)) для прикладного коду, де важливий лише зрозумілий ланцюжок повідомлень. Розуміння ручної реалізації через std::error::Error — фундамент, без якого незрозуміло, що саме автоматизують ці макроси.

Насамкінець варто зафіксувати мінімальний рецепт: #[derive(Debug)] на enum + impl fmt::Display for MyError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { ... write!(f, "...") }}}

  • порожній (або з source) impl std::error::Error for MyError {} — після цього тип автоматично сумісний з ?-конвертацією в Box<dyn Error>, з логуванням через {}/{:?}, і з будь-якою функцією стандартної бібліотеки, що очікує impl Error.
📖 ОПРАЦЮВАТИ

Прочитати документацію std::error::Error на docs.rs/std; реалізувати Display і Error для власного enum помилки з попереднього завдання й перевірити, що його можна обгорнути в Box.

🖥️ ТЕРМІНАЛ — СПРОБУЙ САМ
🎯 ЗАВДАННЯ
  1. Взяти enum ParseError із попередньої теми (або створити новий із трьома варіантами) і реалізувати для нього #[derive(Debug)], ручний impl fmt::Display і порожній impl std::error::Error, перевіривши, що println!("{}", err) і println!("{:?}", err) дають різний, осмислений вивід.
  2. Написати функцію, що повертає Result<T, Box> і всередині використовує оператор ? для двох різних типів помилок (наприклад, io::Error і власний ParseError), не пишучи жодного явного From вручну — переконатися, що це компілюється завдяки бланкетній реалізації From для Box.
  3. Реалізувати метод source() для одного з варіантів enum-помилки, що обгортає io::Error як причину, і написати код, який проходить ланцюжок причин через err.source() у циклі, друкуючи кожен рівень.
  4. Написати той самий власний enum-тип помилки двома способами: вручну (impl Display/Error) і за допомогою #[derive(thiserror::Error)] з атрибутами #[error("...")] (додавши thiserror у Cargo.toml), і порівняти обсяг коду.
  5. Написати невеликий main.rs, що використовує Box як тип повернення функції main() -> Result<(), Box>, навмисно спровокувати помилку (наприклад, читання неіснуючого файлу) і зафіксувати, яке повідомлення й код виходу видає програма.
❓ КОНТРОЛЬНІ ПИТАННЯ
  1. Які два трейти мінімально потрібно реалізувати перед std::error::Error, і яку роль відіграє кожен?
  2. Навіщо потрібен Box<dyn Error> як спільний тип помилки, і завдяки якій механіці стандартної бібліотеки оператор ? автоматично конвертує в нього конкретні типи помилок?
  3. Яку інформацію повертає опційний метод source(), і для чого він потрібен?
  4. У чому компроміс між власним enum-типом помилки та Box<dyn Error> з погляду можливості викликача відрізнити категорії помилок?
  5. Чим призначення крейта thiserror відрізняється від призначення крейта anyhow?
  6. Чому бібліотечний код зазвичай віддає перевагу власному enum-типу помилки, а не Box<dyn Error>, на відміну від прикладного коду?
  7. Що конкретно генерує #[derive(thiserror::Error)], якщо порівняти з ручною реалізацією Display і Error?
✅ САМОПЕРЕВІРКА

Які два трейти мінімально потрібно реалізувати, щоб тип відповідав трейту std::error::Error, і навіщо потрібен Box<dyn Error> як спільний тип помилки?

🥚 ПАСХАЛКА
Якщо пройти ланцюжок source() у власному журналі інцидентів С.І.Д. до кінця, останній рівень завжди той самий: Display каже «сигнал на 101.9 обірвано», а {:?}Signal { block: "Б-7", floor: -3, code: 0xC1D }. Людині — перше, машині — друге, наполягає він; а першопричину інциденту він збирається виправити не через ?, а особисто.
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 3

Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.

cargo run
Збирає й одразу запускає програму; аргументи після -- йдуть програмі (cargo run -- staff.log).
cargo
Система збірки й менеджер пакетів Rust: створює проєкт, тягне залежності (крейти), збирає, тестує, запускає.
cargo build
Компілює проєкт у target/debug/; --release — з оптимізаціями у target/release/.

💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.

← SRS13усі темиSRS15 →