1. Що таке stack trace і чому він важливіший, ніж здається
Коли програма падає, мимоволі хочеться спитати: «Ну й хто це зробив?». Stack trace (його ще називають stack trace, stack backtrace або call stack у момент падіння) — це своєрідний «протокол допиту»: список функцій, які викликали одна одну, аж поки ми не дійшли до рядка, де все зламалося.
Важливо розуміти одну просту річ: рядок, на якому програма впала, часто є місцем прояву симптому, а не першопричиною. Уявіть, що ви наступили на LEGO: це симптом. А першопричина — той, хто залишив LEGO на підлозі. Зазвичай це ви вчора.
У Swift stack trace з’являється у двох ситуаціях. Перша — програма справді зупинилася через trap: наприклад, Int("abc")!, вихід за межі масиву, fatalError, precondition(false). Друга — ви зупинилися в налагоджувачі на брейкпоінті й дивитеся на call stack в IDE: там той самий список, тільки без драматичного падіння.
2. Модель стека викликів: «стопка тарілок» і stack frame
Щоб stack trace не здавався «магічним заклинанням ельфійською», потрібна зрозуміла модель. Уявіть стопку тарілок. Кожна функція — це нова тарілка, яку ми кладемо зверху, коли заходимо у функцію. Коли функція завершується, ми знімаємо тарілку й повертаємося до попередньої. У програмуванні таку «тарілку» називають stack frame (кадр стека): у ньому зберігаються параметри функції, локальні змінні та адреса повернення — тобто місце, куди слід продовжити виконання після return.
Виходить така механіка: main викликає runCLI(), вона — handle(command:), а та — parse(...) і так далі. Чим глибше ми занурюємося в логіку, тим вищою стає стопка.
Невелика схема (дуже спрощено):
flowchart TD
A["main (точка входу)"] --> B["runCLI()"]
B --> C["readCommandLine()"]
B --> D["parseCommand(...)"]
D --> E["handle(command:)"]
E --> F["addBook(...)"]
F --> G["тут стався збій"]
Сенс stack trace — показати вам цей ланцюжок «A → B → C → ... → G», щоб ви могли відповісти на два запитання: де ми впали і хто привів нас до цього місця.
3. Як читати stack trace: верх і низ ланцюжка
У момент падіння stack trace зазвичай виводиться згори вниз, і верхній рядок (або кілька верхніх) — це те місце, де виконання зупинилося. Це найверхніший кадр стека: «ми зараз усередині цієї функції, на цьому рядку».
А нижче йдуть «батьківські» кадри: хто викликав поточну функцію, хто викликав того, хто викликав його, і так далі, аж до точки входу програми та системних кадрів.
Практичний прийом такий. Спочатку знаходите перший кадр, який належить вашому коду: ваш модуль, ваш файл, ваші функції. Усе, що належить Swift runtime і системним бібліотекам, само по собі не є проблемою. Просто майже ніколи це не те місце, де ви помилилися. Далі ви рухаєтеся своїми кадрами знизу вгору й ставите собі запитання: «З якими аргументами я сюди прийшов? Де вони сформувалися?».
Є ще один важливий нюанс. Іноді верхівка стека показує щось на кшталт “Array index out of range” — це правда, але мало корисно, якщо ви не знаєте, який індекс і чому він став таким. Тому якісне налагодження за стеком майже завжди закінчується тим, що ви переходите на кадр нижче й дивитеся, звідки взявся індекс.
4. Мініприклад: робимо падіння і читаємо «маршрут» по стеку
Зараз ми зробимо маленький фрагмент, схожий на те, що ви вже багато разів писали в курсі: «CLI читає рядок → парсить команду → виконує». Ми навмисно залишимо одну «міну» — примусове вилучення !, щоб отримати чесний креш і побачити stack trace.
Уявімо, що в нас є команда додавання книги з роком, і ми вирішили «зекономити час» — улюблена людська помилка — та написали !.
import Foundation
func parseYear(_ text: String) -> Int {
Int(text)! // 💥 якщо text не число — впадемо
}
func addBook(title: String, yearText: String) {
let year = parseYear(yearText)
print("Додано: \(title) (\(year))")
}
func handleAddCommand(_ line: String) {
let parts = line.split(separator: " ")
let title = String(parts[1])
let yearText = String(parts[2])
addBook(title: title, yearText: yearText)
}
handleAddCommand("add Dune oops") // рік не число — провокуємо збій
Цей код компілюється. І саме тому він небезпечний: компілятор не зобов’язаний рятувати нас від логічних рішень «а давай тут буде !».
Як читати stack trace, коли це впаде? У ньому ви побачите ланцюжок функцій, і серед них будуть ваші: parseYear, addBook, handleAddCommand, а потім щось на кшталт main.
Ваша мета — знайти найверхніший кадр із вашим кодом, імовірно parseYear, та зрозуміти: «Окей, ми впали на Int(text)!». Але далі зупинятися не можна. Ви переходите на кадр нижче — addBook — і бачите, що туди прийшов yearText = "oops". Потім — ще нижче, у handleAddCommand, і розумієте: ви взяли третій токен рядка як рік, але користувач увів не число.
Stack trace тут — не про «хто винен», а про те, як саме дані дійшли до помилки.
5. Чому першопричина часто не у верхньому кадрі
Дуже поширена пастка новачка — виправляти верхній кадр так, щоб він «не падав». Наприклад, замінити Int(text)! на Int(text) ?? 0. Програма перестане падати — і це здаватиметься перемогою. Але часто це буде поразка, просто замаскована нулем.
Уявіть: ви додаєте книгу, рік стає 0, книга потрапляє до сховища, потім сортування за роком працює дивно, фільтри показують «книги 0 року», а десь далі все ламається вже без очевидної причини. І все почалося з того, що ми не захотіли чесно обробити помилку.
Правильний спосіб мислити такий: верхній кадр — місце, де «вибухнуло». Наступний кадр — місце, де «принесли бомбу». А ще нижче — місце, де її зібрали. Якщо ви виправляєте лише місце вибуху, не зʼясувавши, хто приніс бомбу і хто її зібрав, ви просто переносите проблему.
Для навчального CLI найчастіше джерела проблем — це парсинг, перетворення типів, індекси та припущення про кількість токенів. Тому під час читання stack trace шукайте в ланцюжку місця на кшталт split, Int(...), array[i], dict[key]! і будь-які «небезпечні операції».
6. Типові падіння в Swift і як вони виглядають у стеку
Stack trace стає легше читати, коли ви впізнаєте типові шаблони. У кожного збою є своя форма, і за цією формою ви часто можете наперед здогадатися, куди дивитися.
Якщо ви бачите збій навколо Optional (!), найчастіше верхній кадр указує на примусове вилучення. Але справжня робота — зʼясувати, чому nil взагалі зʼявився: порожній результат readLine(), відсутній ключ у Dictionary, неправильний формат числа.
Якщо ви бачите щось на кшталт «Index out of range», верхній кадр може бути всередині стандартної бібліотеки, бо саме там відбувається перевірка меж. Ваше завдання — знайти кадр, де ви зверталися до array[index], і подивитися на index та array.count. Майже завжди причина — діапазон 0...array.count замість 0..<array.count або логіка оновлення індексу в циклі.
Якщо ви бачите fatalError(...) або precondition(...), вам зазвичай пощастило: у повідомленні вже написано, що саме очікувалося. Але навіть тут стек важливий: він покаже, хто привів програму в «неможливий стан».
7. Свій код, системні кадри і demangle
Як розшифрувати дивні імена в стеку
Іноді stack trace виглядає привітно: parseYear(_:) і номери рядків. А іноді — так, ніби хтось упустив клавіатуру на ельфійський словник: $s4test28myFunctionDoingTheAllocationyyF. Це не магія, а name mangling — спосіб кодувати імена функцій і типів у символи бінарника.
Хороша новина: це не потрібно розуміти на око. Це потрібно вміти розшифрувати інструментом. У Swift є утиліта swift demangle, яка перетворює закодоване ім’я на читабельне. Це особливо корисно, коли stack trace приходить не з IDE, а із зовнішнього інструмента, наприклад профайлера або системи аналізу помилок, де символи можуть бути не такими «красивими».
Мінінагадування: ми нічого не запускаємо, просто запам’ятовуємо ідею.
// Це не Swift-код, а ідея команди в терміналі:
// swift demangle $s4test28myFunctionDoingTheAllocationyyF
Як швидко виокремлювати «свій» код серед системних кадрів
У реальному проєкті половина стека — це системні виклики. І це нормально: ви ж запускаєте програму не на калькуляторі «Електроніка», а поверх цілої екосистеми.
Щоб не тонути, корисно тримати просте правило: «Мені цікаві кадри, де є мої файли, мій модуль або мої функції». В IDE це зазвичай підсвічено: ви бачите список frames, і серед них частина належить вашому target.
Ось зручна таблиця «що зазвичай означає кадр» — не як суворий закон, а як орієнтир:
| Що ви бачите в кадрі | Що це зазвичай означає | Що робити |
|---|---|---|
| Назва вашої функції та ваш файл | Ви у своєму коді | Це головний кандидат на пошук причини |
| Swift/libswiftCore/runtime | Стандартна бібліотека ловить вашу помилку | Піднімайтеся вище — до вашого кадру, де ви викликали небезпечну операцію |
| main | Точка входу, «початок маршруту» | Це низ стека, корисний для контексту, але рідко причина |
| Дивні символи $s... | Mangling | За потреби розшифруйте їх через swift demangle |
Мета не в тому, щоб вивчити назви системних функцій, а в тому, щоб швидко сказати: «Ага, ось тут починається мій код — звідси й працюємо».
8. Stack trace і рекурсія: коли стек повторюється
Рекурсія — рідкісний звір у прикладному CLI, але як навчальна тема вона вже траплялася, і stack trace для рекурсії особливо показовий. Він буквально показує, як функція викликає саму себе знову і знову. Якщо базовий випадок неправильний, ви побачите дуже багато однакових кадрів.
Мініприклад, який компілюється і допомагає побачити повторюваність кадрів на брейкпоінті або за проблеми з базовим випадком:
import Foundation
func factorial(_ n: Int) -> Int {
if n <= 1 { return 1 }
return n * factorial(n - 1) // стек: factorial → factorial → factorial ...
}
print(factorial(4)) // 24
Якби ви випадково написали return n * factorial(n + 1), стек зростав би до переповнення (stack overflow), і за повторюваними кадрами factorial було б одразу видно: ми не наближаємося до базового випадку.
9. Мініпрактика: токени й помилка меж масиву
Один із найчастіших крешів у CLI-парсингу — неправильні межі масиву токенів. І це майже завжди видно по стеку, якщо ви знаєте, куди дивитися.
Наприклад, ось такий код компілюється і може впасти, якщо команда коротка:
import Foundation
func handleCommand(_ line: String) {
let parts = line.split(separator: " ")
let command = parts[0]
let arg = parts[1] // 💥 якщо аргументу немає — index out of range
print("cmd=\(command), arg=\(arg)")
}
handleCommand("help")
У stack trace ви, найімовірніше, побачите щось про вихід за межі. Але далі важливо знайти кадр із parts[1]. Це і буде «точка симптому». А «точка першопричини» — ваше припущення «у команди завжди є аргумент».
Це припущення потрібно виправляти не магією, а нормальною перевіркою введення — через guard, throws, Result — що вам підходить за архітектурою курсу.
10. Типові помилки під час читання stack trace
Помилка №1: виправляти верхній рядок, не дивлячись на ланцюжок викликів.
Дуже хочеться побачити рядок падіння і одразу переписати його так, щоб «не падало». Але стек майже завжди розповідає, хто передав погані дані. Якщо ви не подивитеся на кадр нижче, ризикуєте сховати проблему під ?? 0 або під «мовчазний return», а потім ловити дивні баги вже в іншому місці.
Помилка №2: лякатися системних кадрів і вважати, що «зламалася стандартна бібліотека».
Стандартна бібліотека рідко ламається сама по собі. Зазвичай вона просто чесно повідомляє: «ти зробив індекс некоректним» або «ти розгорнув nil». У стеку треба знайти перший кадр, де є ваш код, і починати розслідування звідти.
Помилка №3: читати стек як «список підряд» і не розуміти, що кадри — це контексти.
Кожен stack frame — це окремий контекст: свої параметри, свої локальні змінні, своє місце в програмі. Якщо ви перемикаєтеся між кадрами в IDE, не забувайте, що значення змінних змінюються разом із кадром. Дуже часта плутанина — дивитися на змінну не в тому frame і робити хибний висновок.
Помилка №4: ігнорувати mangled-символи й вважати, що стек «нечитабельний».
Так, іноді символи виглядають як $s4test.... Це не привід здаватися. У Swift є прямий спосіб зробити їх читабельними через swift demangle, і цей прийом радять, коли стек містить заманглені імена.
Помилка №5: не відрізняти crash/trap від обробленої помилки.
Якщо у вас throws і ви дійшли до catch, то це не падіння, а нормальний сценарій обробки. Для нього теж корисно дивитися call stack на брейкпоінті, але сенс інший: ви з’ясовуєте, звідки прийшла помилка, а не чому процес завершився. Якщо змішувати ці два світи, можна почати «лікувати» оброблені помилки як креші — наприклад, додаючи ! там, де має бути guard.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ