Bug 與 Debug —
錯誤是朋友
這章結束你會看懂紅字在說什麼,並用 console.log 和二分法一步一步找到壞掉的位置。
開場 · 紅字不是成績單
很多人第一次看到滿版紅字,直覺是:我是不是不適合寫程式?其實剛好相反。紅字不是成績單,也不是電腦在罵你,而是它把「食譜做到哪一步卡住」誠實報告出來。真正會寫程式的人,不是從來不犯錯,而是比較會問:它停在哪一行?是哪一種錯?它到底說了什麼?這章把 bug 當成偵探案,不急著亂改,而是照線索看現場、裝監視器、縮小範圍,最後用一次小測試確認病人真的完全好了。
每個人都會寫出 bug
Bug(錯誤)這個字聽起來很像程式世界的黑話,其實意思很樸素:程式這份食譜裡,有某一步讓電腦做不下去,或做出跟你期待不同的結果。早期電腦真的曾經因為機器裡卡了一隻蟲而故障,Grace Hopper 團隊把它貼在紀錄本上,後來 bug 就成了程式錯誤的常用說法。今天我們寫網頁、遊戲、收銀機計算、表單檢查,bug 不一定是蟲,更多時候是少打一個符號、名字拼錯、資料型態弄混,或規則想反了。
Debug(除錯)不是把自己罵一遍,也不是盯著畫面祈禱它變好。Debug 比較像廚房出菜慢了,主廚回頭查:食材有沒有拿錯?火開了嗎?單子是不是看成隔壁桌?程式也是一樣。你要查的是流程,不是人格。只要你願意把「我寫爛了」換成「這份食譜哪一步和我想的不一樣」,紅字就會變成線報。
完全新手最容易掉進一個坑:看到錯就一次改很多地方。這會讓現場更亂,像偵探還沒拍照就開始搬桌子。成熟的做法是改一處、跑一次、看結果。即使你還不知道怎麼修,先保留錯誤訊息、先記下你剛剛動了哪裡,已經比亂猜強很多。這個習慣到了 AI 幫你寫 code 的時代更重要,因為 AI 像實習生,會寫得很快,也會很快把錯誤擴散。
const step = "把紅字當線索";
console.log("Debug 第一步:" + step);
console.log("改一處,測一次");
紅字三件套:行號、類型、訊息
錯誤訊息(error message)通常不是一整團亂碼,它常常有三個重點。第一是行號:大約告訴你電腦在哪一行停下來。第二是類型:像 SyntaxError、ReferenceError、TypeError,告訴你這次比較像哪一類病。第三是訊息:用英文補一句它覺得哪裡怪。你不需要每個英文單字都背起來,只要先抓這三件事,就能從「看不懂紅字」進步到「知道要查哪個抽屜」。
| 錯誤類型 | 白話意思 | 常見現場 |
|---|---|---|
| SyntaxError | 語法(syntax)錯,食譜句子不合格式 | 少引號、少括號、少逗號 |
| ReferenceError | 找不到你叫的名字 | 變數(variable)拼錯,或還沒建立就拿來用 |
| TypeError | 資料型態(type)不適合這個動作 | 把文字當數字算,或對不存在的東西做動作 |
行號要用聰明的方式看。電腦說第 8 行,不一定代表第 8 行就是真正兇手;有時候第 7 行少了一個括號,電腦到第 8 行才發現句子接不起來。它像導航說「前方路口無法左轉」,問題可能是你上一個路口已經走錯。讀紅字時先看行號附近三到五行,不要只盯著單一行。
訊息裡的英文也有固定味道。Unexpected token 通常像「我在這裡看到不該出現的符號」。is not defined 常常是「這個名字我不認識」。Cannot read properties of undefined 則是「你叫我打開一個根本不存在的盒子」。你不必假裝自己一次看懂全部,只要能把它翻成生活語言,debug 就開始了。
還有一個小技巧:先把錯誤類型原樣留下來,不要急著改成自己的猜測。你可以在筆記裡寫「ReferenceError,提到 medicne」,下一步就去找同名盒子。這種紀錄很短,卻能避免你在五分鐘後忘記剛剛紅字到底說什麼。Debug 不是靠記憶力,是靠留下可檢查的線索。
try {
JSON.parse("{");
} catch (error) {
console.log("錯誤類型:" + error.name);
console.log("訊息開頭:" + error.message.slice(0, 20));
}
console.log:在嫌疑犯旁邊裝監視器
console.log 是新手最該熟的 debug 工具。Console(主控台)可以想成廚房旁邊的小白板,程式做到某一步,你就請它把現在的數字、文字、盒子內容寫出來。很多 bug 難查,不是因為很高深,而是你腦中以為「這裡應該是 300」,實際程式裡卻是「300 元」這串文字。你沒看見現場,就只能猜。
使用 console.log 的方法很直覺:把它放在你懷疑的位置前後。假設收銀機總價怪怪的,你可以印出單價、數量、折扣、最後總價。這就像在嫌疑犯進門前、進門後各裝一支監視器。你不是一次看整間店,而是看資料流過某一段食譜時,有沒有突然變形。
印的時候要加標籤,不要只印一堆數字。console.log(total) 雖然能看,但當畫面有十個數字時,你很快會忘記哪個是哪個。比較好的寫法是 console.log("total", total)。標籤像抽屜上的名字,讓你知道這行輸出在回答哪個問題。也不要害怕多印幾次;debug 時的 log 是臨時監視器,修好後再移除就好。
有一個重要原則:console.log 不是拿來證明你很笨,而是拿來證明資料實際長什麼樣子。當實際輸出和你腦中想像不同,恭喜,你找到線索了。接下來不要急著全改,先問:資料是從哪一步開始變錯?這會自然接到下一節的二分法。
const price = 120;
const count = 3;
const discount = 30;
console.log("單價", price);
console.log("數量", count);
console.log("折扣", discount);
console.log("應付總額", price * count - discount);
二分法:一次切掉一半嫌疑範圍
二分法(binary search thinking)在 debug 裡不是只給數學高手用,而是一種很實用的查案手勢。當一段程式很長,你不知道 bug 躲在哪裡,不要從第一行慢慢猜到最後一行。你可以先在中間放一個 console.log,確認前半段的資料是否正常。如果中間已經錯了,兇手在前半段;如果中間還正常,兇手多半在後半段。一次切掉一半,心裡會安靜很多。
這個方法很像導航找迷路點。你從家出發到餐廳,最後發現到錯地方,不會把每一公尺都重新走一次。你會先問:我到便利商店那個路口時還對嗎?如果對,問題在後面;如果不對,問題在前面。程式的資料也會經過一個個路口,console.log 就是你在路口拍照。
二分法也可以搭配暫時註解。註解(comment)是讓某段 code 暫時不執行的文字標記。當你懷疑某一半造成錯誤,可以暫時關掉那半邊,看看錯誤是否消失。不過新手要記得,註解只是隔離現場,不是正式修法。找到病灶後,要回來把真正的規則修好,別留下大片被關掉的 code,否則下一次你會不知道這份食譜到底少了哪幾步。
如果你面前有三個可能兇手:輸入資料、計算規則、畫面顯示,先不要三個一起改。你可以先印出輸入資料,確認食材對不對;再印出計算後的結果,確認鍋裡有沒有煮壞;最後才看畫面顯示。這種順序能把混亂拆成一格一格的檢查。
用 AI 寫 code 時,二分法尤其有用。AI 可能一次吐出一大段看似漂亮的程式,如果它壞了,你不必整段看懂才開始。先找輸入在哪裡,輸出在哪裡,中間挑一個點印出來。你是在把大問題拆成小問題,這比一直對 AI 說「還是不行」有效得多。
const prices = [100, 200, 300, 400];
const firstHalf = prices.slice(0, 2).reduce((sum, n) => sum + n, 0);
const secondHalf = prices.slice(2).reduce((sum, n) => sum + n, 0);
console.log("前半段合計", firstHalf);
console.log("後半段合計", secondHalf);
console.log("全部合計", firstHalf + secondHalf);
最陰險的是邏輯 bug
有些 bug 很吵,會直接紅字停下來;有些 bug 很安靜,程式照跑,但結果錯了。這種叫邏輯錯誤(logic bug)。例如你想做九折,應該把價格乘以 0.9,卻寫成乘以 1.1;電腦不會報錯,因為這在語法上完全合法。它只會很聽話地把折扣做成加價。邏輯 bug 最陰險的地方,就是它不像 SyntaxError 那樣大聲喊停,而是把錯誤結果包裝得很正常。
另一種常見邏輯 bug 是 off-by-one error(差一錯誤),尤其出現在 ch03 學過的迴圈。你想跑 5 次,結果只跑 4 次,或多跑第 6 次。這通常來自邊界沒有想清楚:從 0 開始還是從 1 開始?條件是小於 5,還是小於等於 5?電腦不會知道你的意思,它只照食譜上的符號走。
對付邏輯 bug,要先寫下期待結果。不要只說「它怪怪的」,要說「輸入 100 和九折時,我期待 90」。期待結果像偵探手上的正確地圖,沒有它,你只是在憑感覺走路。接著用 console.log 看每一步是否靠近期待結果。只要某一步開始偏掉,那一步附近就是你要查的地方。
新手常以為測試一定要很正式,其實一開始可以很生活化。收銀機就拿一筆你心算得出來的交易,猜數字遊戲就拿一個你知道答案的數字,名單篩選就放三個人,其中一個一定該被留下。重點不是測很多,而是先有一個你能判斷對錯的案例。沒有案例,程式跑出任何結果都像合理。
Debug 的最後心法很普通,但很硬:改一處,測一次。你可以把它當成收銀機換零件,每換一個零件就刷一筆測試交易。如果一次換五個零件,就算好了,你也不知道真正原因;如果更壞,你也不知道是哪個改動害的。成熟不是不犯錯,而是讓每次修正都有證據。
const originalPrice = 100;
const expected = 90;
const actual = originalPrice * 0.9;
console.log("期待結果", expected);
console.log("實際結果", actual);
console.log("測試是否通過", actual === expected);
Bug 偵探診所
依序讀病歷、修好 playground 裡的小程式,讓輸出符合治癒條件。每次只改一處,按「跑診斷」看線報。
漏引號的掛號單
一打開就紅字,程式連第一步都做不下去。
純 vibe coder 常把錯誤整段貼給 AI,然後等它猜。你現在會先讀紅字三件套,知道要回報哪一行、哪一類、實際輸出是什麼。你也會用 console.log 和二分法把大段 code 拆小,不再只說「壞了」。這會讓 AI 這位實習生收到更準確的病歷,修錯速度也會明顯變快。
檢查一下這章
下一章預告
下一章進入綠帶:你會看到 AI 幫你寫 code 的時代,真正拉開差距的不是會不會貼 prompt,而是會不會把需求說成可驗證的規格。