Error: Import failed 指向了一行建立錯誤物件的程式碼。開啟這一行,你能看見應用如何報告失敗。要找到真正失敗的操作,還需要沿著附帶的 cause 找到解析器,再檢查傳給它的輸入。

下載完整的 JavaScript 錯誤堆疊練習,解壓後進入 read-javascript-stack-trace 目錄。示例只使用本機檔案和 Node 內建模組,無需安裝依賴,也不發起網路請求。這是為教學構造的例子:匯入步驟唯讀取並解析記錄,不把結果儲存到其他地方。下文命令實際執行於 Node.js 22.22.1;練習要求 Node.js 22 或更新版本。

三個原始碼檔案組成了需要調查的路徑:

檔案 執行的工作 需要核對的證據
run.mjs 選擇輸入,等待匯入結果 輸入檔名、結果、退出碼
importer.mjs 讀取檔案,呼叫解析器,包裝失敗 try 分支和捕獲的 cause
parser.mjs 把文字交給 JSON.parse 解析呼叫及收到的文字

先重現失敗,把兩份堆疊都留下

node run.mjs with-cause

這個命令預期失敗,退出碼為 1。下面合併展示實際執行的標準輸出和錯誤輸出,僅把絕對目錄字首替換成了 file:///lab/;檔名、行列號和錯誤文字都來自這次執行。

Input: records.bad.json
Error: Import failed
    at importRecords (file:///lab/importer.mjs:10:19)
    at async file:///lab/run.mjs:14:19

Caused by:
SyntaxError: Expected double-quoted property name in JSON at position 29 (line 2 column 28)
    at JSON.parse (<anonymous>)
    at parseRecords (file:///lab/parser.mjs:2:15)
    at importRecords (file:///lab/importer.mjs:7:12)
    at async file:///lab/run.mjs:14:19

run.mjs 分別列印了 error.stack 和 error.cause.stack。第一份屬於外層包裝錯誤,第二份屬於被捕獲的 SyntaxError。複製錯誤報告時,要保留這個對應關係。如果日誌只留下外層訊息,這個例子裡大部分有用資訊就丟了。

importer.mjs:10:19 最後的兩個數字是原始碼行號和列號。解析錯誤訊息裡的「line 2 column 28」則指向 JSON 輸入。它們是兩套不同的位置座標,要分別對上相應檔案或值,並使用產生這份堆疊的原始碼版本。

開啟構造錯誤的位置,再讀完整 catch 分支

下載包中的 importer.mjs 全文如下:

import { readFile } from 'node:fs/promises';
import { parseRecords } from './parser.mjs';

export async function importRecords(input, { preserveCause = true } = {}) {
  try {
    const text = await readFile(input, 'utf8');
    return parseRecords(text);
  } catch (cause) {
    const options = preserveCause ? { cause } : undefined;
    const error = new Error('Import failed', options);
    throw error;
  }
}

第 10 行構造外層錯誤,第 11 行才丟擲已有的物件。外層堆疊從第 10 行開始,因為 Node 記錄的是呼叫 new Error() 的位置。稍後再丟擲同一個物件,不會把捕獲的位置移到 throw 那一行。Node.js 22 的錯誤文件說明了這個行為。

還要看 try 包住了什麼:readFile 和 parseRecords 都可能失敗,兩種失敗都會被包裝成相同訊息。只憑「Import failed」無法判斷是哪一步出了問題。例如,練習中的缺失檔案測試得到的是 ENOENT cause,而不是 SyntaxError。

沿 cause 找到解析器,再找到實際輸入

cause 堆疊的第一項是 JSON.parse。繼續看你能開啟的第一處應用程式碼:parser.mjs:2:15。

export function parseRecords(text) {
  return JSON.parse(text);
}

再把 text 追到 readFile(input, 'utf8')。run.mjs 選擇的是 records.bad.json,它的完整內容是:

[
  {"id": "tea", "count": 2,}
]

2 後面的逗號讓解析器繼續等待下一個屬性名,卻在報告的位置遇到了 }。現在你有了具體輸入、失敗操作和可重複的結果。外層錯誤的構造行給了調查入口;修改那條錯誤訊息不會改變這份 JSON。

new Error(message, { cause }) 會讓新錯誤保留對捕獲值的存取。Node 從 16.9.0 開始支援這個選項,本例使用的 22.22.1 文件也包含它。cause 可以是任意值,通用錯誤處理程式碼讀取其 .stack 前應先檢查型別;本例列印器使用 instanceof Error。詳見 Node 的 error.cause 說明。

去掉 cause,看看丟失了什麼

node run.mjs without-cause

它同樣以退出碼 1 結束。相同輸入仍然失敗,但輸出現在只剩下這份錯誤資訊:

Error: Import failed
    at importRecords (file:///lab/importer.mjs:10:19)
    at async file:///lab/run.mjs:14:19

Cause: unavailable

這個模式沒有把 cause 選項傳給 new Error,因此外層錯誤不再攜帶解析錯誤。從它的堆疊中無法找回被丟棄的輸入位置,也無法把本次失敗與缺失檔案分支區分開。你需要補充觀察,例如在本機保留原始錯誤後再次重現。

保留 cause 能改善調查條件,但不會修正輸入:第一個 with-cause 命令仍然失敗。

把非同步幀當線索,再用程式碼補齊路徑

兩份實測堆疊都出現了 at async .../run.mjs:14:19,對應等待結果的呼叫方。解析失敗時,readFile 已經完成。它的工作沒有出現在這份堆疊裡,卻為解析器提供了文字。

V8 的非同步堆疊說明描述了圍繞 await 和部分 Promise 操作恢復非同步幀的能力,包括 Promise.all() 與 Promise.any()。這些幀只覆蓋部分非同步關係,不是所有回撥、定時器和 I/O 的完整執行歷史。少了一幀,也不能證明對應工作沒有發生。

Node/V8 版本可能影響 SyntaxError 的措辭和可見幀。按照實際顯示的位置開啟對應原始碼,不要把本文輸出當成所有環境都相同的格式。練習裡的測試檢查錯誤屬性和有用的應用幀,沒有把完整堆疊字串寫死。

用輸入檢驗「重試就好」的解釋

考慮這樣一個說法:「這是臨時網路問題,重試應該就能恢復。」這裡把它作為需要驗證的教學假設,沒有把它當成真實 AI 輸出或線上故障記錄。

堆疊給出的是 JSON.parse 處的 SyntaxError,原始碼說明解析器讀到的位元組來自本機檔案。這個練習沒有網路操作;同一份壞輸入連續嘗試三次,都仍然失敗。這些證據不支援上述解釋。

現在執行修正後的輸入;它只刪除了多餘的尾逗號:

node run.mjs fixed

命令退出碼為 0,輸出為:

Input: records.fixed.json
Parsed records: [{"id":"tea","count":2}]

再執行回歸檢查:

node --test importer.test.mjs

九個測試全部透過,覆蓋保留解析 cause、丟失 cause 的包裝、相同輸入重試失敗、修正輸入成功、缺失檔案的不同 cause、構造位置與稍後丟擲的位置,以及三種 CLI 模式。兩個失敗演示命令是預期結果,不表示回歸測試失敗。

這些結果解釋了本例的本機輸入。調查真實匯入流程時,還要繼續讀產生異常文字的程式碼,再判斷文字為什麼不合法。這個練習只驗證 JSON 語法和示例結果,不檢查記錄 schema,也不儲存匯入資料。

在 Yenpo 中讀上下文,在終端裡驗證

在 Yenpo 開啟解壓後的目錄,按照程式碼閱讀指南定位 importer.mjs、parser.mjs 和選中的輸入。先看報告的行號,再讀完整 catch 分支,以及提供 input 的呼叫方。

設定模型後,可以讓 Yenpo AI Reader說明檢查這條路徑。一個具體的請求是:

從 Import failed 的構造位置沿 cause 追到傳給 JSON.parse 的輸入,引用對應檔案和行號。區分堆疊直接顯示了什麼、原始碼能證明什麼,以及哪些結論還需要在終端執行確認。檢查本例中重試能否改變輸入位元組。

在終端機執行命令,對照輸出檢查解釋。