Node.js Async / Await:寫出如同步般的非同步程式碼

Async / Await 是在 ES2017 (ES8) 引入的重磅功能。它建立了 Promise 之上,但卻讓非同步程式碼讀起來跟「同步」程式一樣自然、易於理解。這也是目前 Node.js 社群最推崇的開發模式。

基本語法與運作原理

  • async 關鍵字:標記在函式前面,表示該函式是非同步的。async 函式保證會回傳一個 Promise,若回傳的是一般值,它會自動被包裝成 Promise.resolve(值)
  • await 關鍵字:只能在 async 函式內使用。它會「暫停」該函式的執行,直到 Promise 完成並回傳結果。
重點await 雖然會暫停該 async 函式的進度,但不會阻塞 (Block) Node.js 的主執行緒(Event Loop)。在此期間,伺服器依然可以處理其他使用者的請求。

寫法對比

傳統 Promise 寫法:

function getUserData() {
  getUser().then((user) => {
    console.log(user);
  });
}

現代 Async / Await 寫法:

async function getUserData() {
  const user = await getUser();
  console.log(user);
}

錯誤處理:回歸傳統的 Try...Catch

Async / Await 讓錯誤處理變得非常優雅。你不需要在鏈結尾端掛載 .catch(),而是使用傳統的 try...catch 結構。

async function fetchConfig() {
  try {
    const data = await fs.readFile('config.json');
    const config = JSON.parse(data);
    return config;
  } catch (error) {
    console.error('讀取設定檔發生錯誤:', error.message);
    // 可以在這裡進行錯誤修復或拋出更具義意的錯誤
  }
}

效能警示:別掉入「順序執行」的陷阱

這是開發者最常犯的效能開發錯誤。如果你有三個互相獨立的任務,請不要一個接一個地 await

錯誤範例(總耗時過長):

// 每個任務耗時 1 秒,總共浪費 3 秒
const user = await fetchUser();
const posts = await fetchPosts();
const stats = await fetchStats();

正確範例(並列執行,僅耗時 1 秒):

// 同時發送請求,效率最高
const [user, posts, stats] = await Promise.all([fetchUser(), fetchPosts(), fetchStats()]);

Top-level Await (頂層 Await)

在現代 Node.js 版本中(特別是在使用 ES Modules 模式時),你不需要將代碼包裹在函數內,可以直接在檔案的最頂層使用 await。這對於編寫簡單的腳本或進行初始化操作非常方便。

// 僅限 ESM 或 Node.js REPL 模式
import db from './database.js';

await db.connect();
console.log('資料庫連線已就緒!');

總結

  1. Async / Await 本質上就是 Promise,但它提供了更好的可讀性。
  2. 它讓非同步邏輯具備良好的線性視覺順序(由上到下)。
  3. 配合 try...catch 讓錯誤處理邏輯與業務邏輯分離得更乾淨。