跳到主要内容

JavaScript 错误处理

错误处理是编写健壮 JavaScript 应用的重要部分。本章介绍如何正确处理和抛出错误。

错误类型

JavaScript 有多种内置的错误类型,每种类型代表不同类型的错误。

Error

Error 是所有错误类型的基类:

const error = new Error("这是一个错误");
console.log(error.name); // "Error"
console.log(error.message); // "这是一个错误"
console.log(error.stack); // 错误堆栈信息

常见错误类型

// SyntaxError - 语法错误
// const a = ; // SyntaxError: Unexpected token ';'

// ReferenceError - 引用错误
// console.log(undefinedVar); // ReferenceError: undefinedVar is not defined

// TypeError - 类型错误
// null.foo; // TypeError: Cannot read property 'foo' of null

// RangeError - 范围错误
// const arr = new Array(-1); // RangeError: Invalid array length

// URIError - URI 错误
// decodeURI('%'); // URIError: URI malformed

// EvalError - eval 错误(很少使用)

自定义错误类型

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

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

function validateUser(user) {
if (!user.name) {
throw new ValidationError("用户名不能为空", "name");
}
if (!user.email) {
throw new ValidationError("邮箱不能为空", "email");
}
}

try {
validateUser({ name: "", email: "[email protected]" });
} catch (error) {
if (error instanceof ValidationError) {
console.log(`验证错误:${error.field} - ${error.message}`);
} else {
throw error;
}
}

try...catch...finally

try...catch 语句用于捕获和处理异常。

基本语法

try {

} catch (error) {

} finally {

}

基本用法

function divide(a, b) {
if (b === 0) {
throw new Error("除数不能为零");
}
return a / b;
}

try {
const result = divide(10, 0);
console.log(result);
} catch (error) {
console.error("发生错误:", error.message);
}

catch 块

catch 块接收一个参数,即抛出的错误对象:

try {
JSON.parse("无效的 JSON");
} catch (error) {
console.log(error.name); // "SyntaxError"
console.log(error.message); // 错误信息
console.log(error.stack); // 堆栈跟踪
}

在 ES2019 之前,catch 必须有参数。ES2019 之后可以省略:

try {
doSomething();
} catch {
console.log("发生错误,但不需要错误对象");
}

finally 块

finally 块无论是否发生错误都会执行,常用于清理资源:

let connection = null;

try {
connection = openConnection();
connection.sendData(data);
} catch (error) {
console.error("发送失败:", error.message);
} finally {
if (connection) {
connection.close();
}
console.log("连接已关闭");
}

finally 的返回值

finally 块中的 return 会覆盖 try 和 catch 中的返回值:

function test() {
try {
return "来自 try";
} finally {
return "来自 finally";
}
}

console.log(test());

throw 语句

throw 语句用于抛出异常。

抛出错误对象

throw new Error("出错了");
throw new TypeError("类型错误");
throw new RangeError("范围错误");

抛出任意值

JavaScript 允许抛出任意类型的值:

throw "错误消息";
throw 404;
throw { code: 500, message: "服务器错误" };
throw ["错误1", "错误2"];

但推荐始终抛出 Error 对象或其子类,这样可以保留堆栈信息:

function processAge(age) {
if (typeof age !== "number") {
throw new TypeError("年龄必须是数字");
}
if (age < 0 || age > 150) {
throw new RangeError("年龄必须在 0-150 之间");
}
return age;
}

重新抛出错误

可以在 catch 块中重新抛出错误:

function processJson(jsonString) {
try {
return JSON.parse(jsonString);
} catch (error) {
if (error instanceof SyntaxError) {
throw new Error("JSON 格式无效: " + error.message);
}
throw error;
}
}

错误处理的最佳实践

1. 只捕获你能处理的错误

function fetchUser(id) {
try {
const user = getUserFromDatabase(id);
return user;
} catch (error) {
if (error.code === "DATABASE_CONNECTION_ERROR") {
return null;
}
throw error;
}
}

2. 提供有意义的错误信息

function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new Error(`无效的邮箱地址: "${email}",请输入正确的邮箱格式`);
}
}

3. 使用自定义错误类型

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

class DatabaseError extends AppError {
constructor(message, query) {
super(message, "DATABASE_ERROR");
this.query = query;
this.name = "DatabaseError";
}
}

class AuthenticationError extends AppError {
constructor(message) {
super(message, "AUTH_ERROR");
this.name = "AuthenticationError";
}
}

function handleAppError(error) {
if (error instanceof AuthenticationError) {
redirectToLogin();
} else if (error instanceof DatabaseError) {
showDatabaseErrorPage();
} else {
showGenericErrorPage();
}
}

4. 记录错误日志

function logError(error, context = {}) {
const errorLog = {
timestamp: new Date().toISOString(),
name: error.name,
message: error.message,
stack: error.stack,
context: context,
userAgent: navigator.userAgent,
url: window.location.href
};

console.error("错误日志:", errorLog);

sendToErrorTrackingService(errorLog);
}

try {
processPayment(order);
} catch (error) {
logError(error, { orderId: order.id, amount: order.amount });
throw error;
}

5. 不要用 try...catch 控制流程

function isNumeric(value) {
return typeof value === "number" && !isNaN(value);
}

function sumArray(arr) {
if (!Array.isArray(arr)) {
throw new TypeError("参数必须是数组");
}

let sum = 0;
for (const item of arr) {
if (!isNumeric(item)) {
throw new TypeError(`数组包含非数字元素: ${item}`);
}
sum += item;
}
return sum;
}

异步错误处理

Promise 错误处理

fetch("/api/users")
.then(response => {
if (!response.ok) {
throw new Error(`HTTP 错误: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error("请求失败:", error.message);
});

async/await 错误处理

async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);

if (!response.ok) {
throw new Error(`HTTP 错误: ${response.status}`);
}

const user = await response.json();
return user;
} catch (error) {
if (error instanceof TypeError && error.message.includes("fetch")) {
console.error("网络错误:", error.message);
} else {
console.error("获取用户失败:", error.message);
}
throw error;
}
}

async function main() {
try {
const user = await fetchUser(1);
console.log(user);
} catch (error) {
console.error("主流程错误:", error);
}
}

处理多个异步操作

async function fetchAllData() {
try {
const [users, posts, comments] = await Promise.all([
fetch("/api/users").then(r => r.json()),
fetch("/api/posts").then(r => r.json()),
fetch("/api/comments").then(r => r.json())
]);

return { users, posts, comments };
} catch (error) {
console.error("获取数据失败:", error);
throw error;
}
}

async function fetchAllDataSafe() {
const results = await Promise.allSettled([
fetch("/api/users").then(r => r.json()),
fetch("/api/posts").then(r => r.json()),
fetch("/api/comments").then(r => r.json())
]);

const data = {};
const errors = [];

results.forEach((result, index) => {
const keys = ["users", "posts", "comments"];
if (result.status === "fulfilled") {
data[keys[index]] = result.value;
} else {
errors.push({ key: keys[index], error: result.reason });
}
});

if (errors.length > 0) {
console.warn("部分请求失败:", errors);
}

return data;
}

全局 Promise 错误处理

window.addEventListener("unhandledrejection", function(event) {
console.error("未处理的 Promise 拒绝:", event.reason);

event.preventDefault();

reportErrorToService(event.reason);
});

window.addEventListener("rejectionhandled", function(event) {
console.log("Promise 拒绝已被处理:", event.reason);
});

全局错误处理

window.onerror

window.onerror = function(message, source, lineno, colno, error) {
console.error("全局错误:", {
message: message,
source: source,
line: lineno,
column: colno,
error: error
});

return true;
};

window.addEventListener('error')

window.addEventListener("error", function(event) {
console.error("捕获到错误:", {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
error: event.error
});

event.preventDefault();
});

资源加载错误

window.addEventListener("error", function(event) {
if (event.target !== window) {
console.error("资源加载失败:", event.target.src || event.target.href);
}
}, true);

document.querySelector("img").addEventListener("error", function(event) {
event.target.src = "/images/placeholder.png";
});

错误上报

错误上报函数

class ErrorReporter {
constructor(endpoint) {
this.endpoint = endpoint;
this.queue = [];
this.init();
}

init() {
window.addEventListener("error", (event) => {
this.report({
type: "error",
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack
});
});

window.addEventListener("unhandledrejection", (event) => {
this.report({
type: "unhandledrejection",
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack
});
});
}

report(errorData) {
const payload = {
...errorData,
timestamp: new Date().toISOString(),
url: window.location.href,
userAgent: navigator.userAgent
};

if (navigator.sendBeacon) {
navigator.sendBeacon(this.endpoint, JSON.stringify(payload));
} else {
fetch(this.endpoint, {
method: "POST",
body: JSON.stringify(payload),
keepalive: true
});
}
}
}

const reporter = new ErrorReporter("/api/errors");

实战示例

API 请求封装

class ApiClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}

async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;

try {
const response = await fetch(url, {
headers: {
"Content-Type": "application/json",
...options.headers
},
...options
});

if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new ApiError(
errorData.message || `HTTP 错误: ${response.status}`,
response.status,
errorData
);
}

return await response.json();
} catch (error) {
if (error instanceof ApiError) {
throw error;
}

if (error.name === "AbortError") {
throw new ApiError("请求被取消", 0, {});
}

if (error.name === "TypeError") {
throw new ApiError("网络错误,请检查网络连接", 0, {});
}

throw new ApiError("未知错误", 0, {});
}
}

get(endpoint) {
return this.request(endpoint, { method: "GET" });
}

post(endpoint, data) {
return this.request(endpoint, {
method: "POST",
body: JSON.stringify(data)
});
}
}

class ApiError extends Error {
constructor(message, status, data) {
super(message);
this.name = "ApiError";
this.status = status;
this.data = data;
}
}

const api = new ApiClient("/api");

async function loadUserProfile() {
try {
const user = await api.get("/user/profile");
return user;
} catch (error) {
if (error instanceof ApiError) {
if (error.status === 401) {
redirectToLogin();
} else if (error.status === 404) {
showNotFoundPage();
} else {
showErrorMessage(error.message);
}
}
throw error;
}
}

表单验证

class FormValidator {
constructor(rules) {
this.rules = rules;
this.errors = {};
}

validate(data) {
this.errors = {};

for (const [field, fieldRules] of Object.entries(this.rules)) {
for (const rule of fieldRules) {
const error = this.validateRule(field, data[field], rule);
if (error) {
this.errors[field] = error;
break;
}
}
}

return Object.keys(this.errors).length === 0;
}

validateRule(field, value, rule) {
switch (rule.type) {
case "required":
if (!value || value.trim() === "") {
return rule.message || `${field} 是必填项`;
}
break;

case "email":
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (value && !emailRegex.test(value)) {
return rule.message || `${field} 格式不正确`;
}
break;

case "minLength":
if (value && value.length < rule.value) {
return rule.message || `${field} 至少需要 ${rule.value} 个字符`;
}
break;

case "maxLength":
if (value && value.length > rule.value) {
return rule.message || `${field} 最多允许 ${rule.value} 个字符`;
}
break;

case "pattern":
if (value && !rule.value.test(value)) {
return rule.message || `${field} 格式不正确`;
}
break;

case "custom":
return rule.validator(value, field);
}

return null;
}

getErrors() {
return this.errors;
}

hasError(field) {
return field in this.errors;
}

getError(field) {
return this.errors[field];
}
}

const validator = new FormValidator({
name: [
{ type: "required", message: "请输入姓名" },
{ type: "minLength", value: 2, message: "姓名至少 2 个字符" }
],
email: [
{ type: "required", message: "请输入邮箱" },
{ type: "email", message: "邮箱格式不正确" }
],
password: [
{ type: "required", message: "请输入密码" },
{ type: "minLength", value: 8, message: "密码至少 8 个字符" },
{
type: "custom",
validator: (value) => {
if (!/[A-Z]/.test(value)) return "密码必须包含大写字母";
if (!/[a-z]/.test(value)) return "密码必须包含小写字母";
if (!/[0-9]/.test(value)) return "密码必须包含数字";
return null;
}
}
]
});

function handleSubmit(formData) {
if (validator.validate(formData)) {
submitForm(formData);
} else {
showFormErrors(validator.getErrors());
}
}

小结

  1. JavaScript 有多种内置错误类型:Error、TypeError、ReferenceError 等
  2. 使用 try...catch...finally 捕获和处理同步错误
  3. 使用 throw 抛出错误,推荐抛出 Error 对象
  4. 异步错误需要用 Promise.catch() 或 async/await 配合 try...catch
  5. 使用全局错误处理捕获未处理的错误
  6. 创建自定义错误类型来区分不同类型的错误
  7. 记录和上报错误有助于调试和改进应用

练习

  1. 创建一个自定义错误类型 ValidationError,包含字段名和错误信息
  2. 实现一个带重试机制的 API 请求函数
  3. 实现一个全局错误上报系统
  4. 编写一个表单验证器,支持多种验证规则
  5. 实现一个错误边界组件,捕获子组件的错误并显示备用 UI