Skip to content
⚠️ This article was written in 2018. Some content may be outdated.

JavaScript 錯誤處理最佳實踐

項目上線後出 bug,最痛苦的不是有 bug,而是不知道 bug 在哪。良好的錯誤處理讓問題更容易定位。

同步錯誤:try/catch

javascript
// 基礎用法
try {
  JSON.parse("invalid json");
} catch (e) {
  console.error("JSON 解析失敗:", e.message);
}

// finally:不管成功還是失敗都會執行
function readFile() {
  let file = null;
  try {
    file = openFile("data.json");
    return parseContent(file);
  } catch (e) {
    console.error("讀取失敗:", e);
    throw e; // 重新拋出,讓調用者知道失敗了
  } finally {
    if (file) file.close(); // 確保資源被釋放
  }
}

異步錯誤處理

javascript
// Promise:用 .catch() 捕獲
fetchUser(id)
  .then((user) => renderUser(user))
  .catch((e) => {
    console.error("獲取用户失敗:", e);
    showErrorMessage("加載失敗,請重試");
  });

// async/await:用 try/catch
async function loadUser(id) {
  try {
    const user = await fetchUser(id);
    return user;
  } catch (e) {
    if (e.status === 404) {
      return null; // 用户不存在,返回 null 而不是拋出
    }
    throw e; // 其他錯誤,繼續向上傳播
  }
}

自定義錯誤類

javascript
// 區分不同類型的錯誤
class ApiError extends Error {
  constructor(message, status, code) {
    super(message);
    this.name = "ApiError";
    this.status = status;
    this.code = code;
  }
}

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = "ValidationError";
    this.field = field;
  }
}

// 使用
async function createUser(data) {
  if (!data.email) {
    throw new ValidationError("郵箱不能為空", "email");
  }

  const res = await fetch("/api/users", {
    method: "POST",
    body: JSON.stringify(data),
  });
  if (!res.ok) {
    const body = await res.json();
    throw new ApiError(body.message, res.status, body.code);
  }

  return res.json();
}

// 調用方可以精確處理不同類型的錯誤
try {
  await createUser({ name: "張三" });
} catch (e) {
  if (e instanceof ValidationError) {
    formErrors[e.field] = e.message; // 顯示字段錯誤
  } else if (e instanceof ApiError) {
    message.error(e.message); // 顯示 API 錯誤
  } else {
    message.error("未知錯誤,請刷新頁面");
    Sentry.captureException(e); // 上報未知錯誤
  }
}

全局錯誤捕獲

javascript
// 捕獲未處理的 Promise 拒絕
window.addEventListener("unhandledrejection", (event) => {
  console.error("未處理的 Promise 拒絕:", event.reason);
  Sentry.captureException(event.reason);
  event.preventDefault(); // 阻止默認的控制台警告
});

// 捕獲同步錯誤(運行時錯誤)
window.addEventListener("error", (event) => {
  if (event.error) {
    Sentry.captureException(event.error);
  }
});

Vue 的錯誤處理

javascript
// main.js
Vue.config.errorHandler = (err, vm, info) => {
  // 組件內的錯誤都會被這裏捕獲(生產環境)
  console.error("Vue 組件錯誤:", err, info);
  Sentry.captureException(err, {
    extra: { componentInfo: info },
  });
};

// 組件內:errorCaptured 鈎子
export default {
  errorCaptured(err, vm, info) {
    // 捕獲子組件的錯誤
    this.error = err.message;
    return false; // 阻止錯誤繼續向上傳播
  },
};

錯誤邊界組件

javascript
// 包裹容易出錯的區域,出錯後降級顯示
Vue.component("ErrorBoundary", {
  data() {
    return { error: null };
  },
  errorCaptured(err) {
    this.error = err;
    return false;
  },
  render(h) {
    if (this.error) {
      return h("div", { class: "error-fallback" }, [
        h("p", "加載失敗"),
        h(
          "button",
          {
            on: {
              click: () => {
                this.error = null;
              },
            },
          },
          "重試",
        ),
      ]);
    }
    return this.$slots.default[0];
  },
});

小結

  • 根據錯誤類型做不同處理,不要一律 console.error 就完了
  • 自定義錯誤類讓調用方可以精確處理不同情況
  • unhandledrejection 捕獲漏掉 .catch() 的 Promise
  • Vue errorHandler 捕獲組件樹內的所有錯誤
  • 生產環境接入 Sentry 或類似工具,讓錯誤可見

MIT Licensed