Debugger to narzędzie, które zatrzymuje działający program w wybranym miejscu i pozwala po kolei wykonywać kod oraz podglądać wartości zmiennych. Dzięki temu widzisz, co program faktycznie robi, a nie co Ci się wydaje, że robi. Podstawą są trzy rzeczy: punkt zatrzymania (breakpoint), krokowanie i podgląd stosu wywołań (call stack).
Pierwszy błąd w kodzie prawie każdy szuka tak samo: dopisuje print (w Pythonie) albo console.log (w JavaScripcie), czyli polecenie wypisujące wartość na ekran, uruchamia program i patrzy, co wyszło. To działa, dopóki program jest mały. Gdy robi się większy, z tego powstaje dwadzieścia wydruków, z których żaden nie odpowiada na pytanie, dlaczego. Debugger jest narzędziem, które pozwala zadać to pytanie wprost.
Co to jest debugger?
Debugger to program (albo funkcja edytora czy przeglądarki), który uruchamia Twój kod pod kontrolą. Możesz go zatrzymać w dowolnym miejscu, wykonać kolejną linię, wejść do funkcji i sprawdzić wartość każdej zmiennej. Kod nie musi się przy tym zmieniać: nie dopisujesz niczego i nie musisz tego potem usuwać.
Do debugowania (po angielsku debugging) nie potrzebujesz niczego poza tym, co już masz. Debuggery wbudowane są w przeglądarki (DevTools), w VS Code i w Pythona (pdb).
Skąd się wzięło słowo „bug” i „debugging”?
Wiele osób zna historię o ćmie. Prawdziwa wersja jest ciekawsza, bo słowo jest starsze od komputerów.
- 1878. Thomas Edison w liście do współpracownika pisze o „bugach” (Bugs) jako o małych usterkach i trudnościach. Słowo było więc używane w inżynierii na długo przed pierwszym komputerem.
- Około 1947. Operatorzy komputera Harvard Mark II znaleźli ćmę uwięzioną w przekaźniku i dołączyli ją do dziennika z podpisem „First actual case of bug being found”. Anegdota jest często opowiadana tak, jakby wtedy powstało słowo „bug”, a to nieprawda: dziennik opisuje dosłowny przypadek żartobliwego użycia istniejącego już określenia. Rok i okoliczności różnią się zależnie od źródła (podawane są lata 1945–1947), więc traktuj datę orientacyjnie.
- 1945. Słownik OED notuje słowo debugging już w kontekście silników lotniczych, czyli też przed erą oprogramowania.
- 1959–1961. Pierwsze interaktywne debuggery powstały na MIT: FLIT dla komputera TX-0 (1959) i DDT dla PDP-1 (1961). Można było zatrzymać program i sprawdzić jego stan zamiast czytać wydruk po zakończeniu.
- 1986. Richard Stallman pisze GDB, symboliczny debugger dla języka C, wzorowany na debuggerze DBX z Uniksa Berkeley.
- 2006. Joe Hewitt tworzy Firebug, dodatek do Firefoksa, który dał webdeweloperom podgląd kodu, DOM-u i debugger wprost w przeglądarce. Projekt wygasł w 2017 roku, kiedy podobne narzędzia weszły do Firefoksa i innych przeglądarek jako wbudowane DevTools.
Z czego składa się debugger? Najważniejsze pojęcia
Niezależnie od języka debugger oferuje ten sam zestaw. Różnią się tylko przyciski i skróty:
| Pojęcie | Co robi | DevTools / VS Code | pdb (Python) |
|---|---|---|---|
| Punkt zatrzymania (breakpoint) | zatrzymuje program w wybranej linii | klik w numer linii | b plik.py:10 |
| Wznów (continue) | jedzie do następnego breakpointu | F8 (DevTools), F5 (VS Code) | c |
| Krok dalej (step over) | wykonuje linię, nie wchodzi do funkcji | F10 | n |
| Krok w głąb (step into) | wchodzi do wywoływanej funkcji | F11 | s |
| Krok na zewnątrz (step out) | kończy bieżącą funkcję i wraca do wywołującej | Shift+F11 | r |
| Stos wywołań (call stack) | pokazuje łańcuch funkcji, które doprowadziły do tego miejsca | panel Call Stack | w |
| Podgląd zmiennych (watch) | pokazuje wartość zmiennej lub wyrażenia | panel Scope / Watch | p zmienna |
| Warunkowy breakpoint (conditional breakpoint) | zatrzymuje się tylko, gdy warunek jest prawdziwy | PPM na numerze linii → warunkowy | b plik.py:10, i == 99 |
Skróty klawiszowe są domyślne i mogą się różnić zależnie od systemu i ustawień. Szczegóły narzędzi opisują osobne artykuły: o debugowaniu w JavaScript i o debugowaniu w Pythonie.
Czy debugger jest lepszy od print i console.log?
Zależy od błędu, ale w większości przypadków tak. Wydruk pokazuje to, co zdecydowałeś się wypisać, w momencie, który wybrałeś z góry. Debugger pozwala zadawać pytania po tym, jak program stanął, i na tym polega różnica.
Przykład w Pythonie. Funkcja dodaje tagi do wpisu, ale drugi wpis dostaje tag z pierwszego:
def add_tag(tag, tags=[]):
tags.append(tag)
return tags
def build_post(title, tag):
return {"title": title, "tags": add_tag(tag)}
print(build_post("Pierwszy", "python"))
print(build_post("Drugi", "js"))
{'title': 'Pierwszy', 'tags': ['python']}
{'title': 'Drugi', 'tags': ['python', 'js']}
Dopisanie print w funkcji pokaże, że przy drugim wywołaniu lista już zawiera 'python'. To objaw, ale nadal nie wiesz dlaczego. W debuggerze zatrzymujesz program w linii z tags.append i sprawdzasz nie tylko zmienną, ale też samą funkcję:
python -m pdb run.py
(Pdb) b tags.py:2
(Pdb) c
(Pdb) p tags
[]
(Pdb) c
(Pdb) p tags
['python']
(Pdb) p add_tag.__defaults__
(['python'],)
Ostatnia linia pokazuje przyczynę: domyślna wartość argumentu (tags=[]) jest jedną listą, utworzoną raz przy definicji funkcji i współdzieloną między wywołaniami. Poprawka to tags=None i utworzenie listy wewnątrz funkcji. Gdyby wiedza o tej pułapce była Ci znana, print też by wystarczył. Debugger pomaga wtedy, gdy jeszcze jej nie znasz.
Przykład w JavaScripcie. Funkcja sumuje ceny, ale zamiast 35 zwraca dziwny napis:
function totalPrice(prices) {
let total = 0;
for (const price of prices) {
debugger;
total += price;
}
return total;
}
console.log(totalPrice(["10", "20", "5"]));
010205
Instrukcja debugger; zatrzymuje program w przeglądarce (gdy DevTools są otwarte) i w Node uruchomionym przez node inspect. W drugiej iteracji pętli widać:
debug> exec total
'010'
debug> exec typeof total
'string'
Cena "10" jest napisem, a 0 + "10" daje "010", nie liczbę. Od tego miejsca += skleja napisy zamiast dodawać. Debugger pokazał typ zmiennej, o który nikt nie pytał, bo nie miał skąd wiedzieć, że trzeba go zapytać.
Nie znaczy to, że print jest zły. Do szybkiego sprawdzenia jednej wartości w małym skrypcie wystarcza, a w środowiskach bez debuggera bywa jedynym wyborem. Debugger oszczędza czas, gdy nie wiesz, czego szukasz.
Jak krok po kroku znaleźć błąd debuggerem?
- Odtwórz błąd. Uruchom program tak, żeby błąd wystąpił za każdym razem.
- Ustaw breakpoint przed miejscem, gdzie wynik jest już zły, a nie na samym błędzie. Jeśli nie wiesz gdzie, zacznij od początku funkcji, która zwraca zły wynik.
- Krokuj i porównuj. Po każdym kroku sprawdź, czy wartości są takie, jakich się spodziewasz. Pierwsze miejsce, w którym nie są, to Twój błąd albo jego bezpośrednia przyczyna.
- Sprawdź stos wywołań. Jeśli zły argument przyszedł z innej funkcji, call stack pokazuje, z której.
- Popraw i potwierdź. Uruchom ten sam scenariusz jeszcze raz, a najlepiej zapisz go jako test, żeby błąd nie wrócił.
Ten proces ma jeden szczególny przypadek: gdy kod napisało AI, a błędu nie widać w komunikacie. Opisuje go artykuł o debugowaniu kodu z AI.
Jak odpowiedzieć na pytanie rekrutacyjne: „Jak zweryfikujesz błąd i go poprawisz?”
To pytanie regularnie pada na rozmowach na stanowiska juniorskie. Rekruter albo programista techniczny nie pyta o konkretne narzędzie, tylko o to, czy masz sposób działania. Dobra odpowiedź to proces, a nie „sprawdzę w konsoli”.
Odpowiedź w pięciu krokach, którą możesz powiedzieć własnymi słowami:
- Odtwarzam błąd. Ustalam, w jakich warunkach występuje, i zapisuję najmniejszy przypadek, który go wywołuje.
- Czytam komunikat i stos wywołań. Sprawdzam, gdzie błąd wyszedł na jaw, pamiętając, że nie zawsze tam powstał.
- Zawężam przyczynę debuggerem. Ustawiam breakpoint, krokuję i porównuję wartości z oczekiwanymi. Szukam pierwszego miejsca, gdzie stan programu przestaje się zgadzać.
- Poprawiam przyczynę, nie objaw. Zamiast łatać wynik, naprawiam miejsce, gdzie błąd powstał.
- Weryfikuję i zabezpieczam. Uruchamiam ten sam scenariusz, dodaję test, który by padł przed poprawką, i sprawdzam, czy nie zepsułem czegoś obok.
Co sprawia, że ta odpowiedź robi wrażenie? Wymienia kolejność działań i pokazuje, że wiesz, po co jest każdy krok. Dobrze jest też dodać krótki przykład z własnego projektu: jaki to był błąd, co pokazał debugger i jak potwierdziłeś poprawkę. Więcej o przygotowaniu do rozmów opisuje artykuł rekrutacja junior developera: jak się przygotować.
Jakie są najczęstsze błędy początkujących w debugowaniu?
- Poprawianie bez odtworzenia błędu. Bez powtarzalnego scenariusza nie wiesz, czy poprawka zadziałała.
- Breakpoint zbyt późno. Ustawiony na linii, w której błąd już jest widoczny, pokazuje skutek, a nie przyczynę.
- Krokowanie „na ślepo”. Naciskasz F10 po kolei, nie patrząc na zmienne. Debugger nie szuka błędu za Ciebie, tylko pokazuje stan, który musisz sam ocenić.
- Wchodzenie w każdą funkcję. Funkcje z biblioteki zwykle omijasz (step over), a wchodzisz tylko do własnych.
- Zostawione
debugger;w kodzie. W JavaScript instrukcja zatrzymuje wykonanie, gdy DevTools są otwarte. Usuń ją przed commitem. - Zgadywanie zamiast obserwacji. Zmieniasz kod „na próbę”, aż coś zadziała. Zwykle tylko przesuwasz błąd.
Podsumowanie
Debugger to sposób na zadawanie pytań działającemu programowi. Naucz się trzech rzeczy: breakpointów, krokowania i call stacku. Resztę dobierzesz do języka. Dalej:
- w JavaScripcie debugger masz w przeglądarce i w VS Code, o czym piszę w artykule o debugowaniu w JavaScript,
- w Pythonie masz
pdbi VS Code, a opisuje je artykuł o debugowaniu w Pythonie, - skróty edytora, które przyspieszają pracę, opisuje artykuł o skrótach w VS Code.


