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());
}
}
小结
- JavaScript 有多种内置错误类型:Error、TypeError、ReferenceError 等
- 使用
try...catch...finally捕获和处理同步错误 - 使用
throw抛出错误,推荐抛出 Error 对象 - 异步错误需要用 Promise.catch() 或 async/await 配合 try...catch
- 使用全局错误处理捕获未处理的错误
- 创建自定义错误类型来区分不同类型的错误
- 记录和上报错误有助于调试和改进应用
练习
- 创建一个自定义错误类型
ValidationError,包含字段名和错误信息 - 实现一个带重试机制的 API 请求函数
- 实现一个全局错误上报系统
- 编写一个表单验证器,支持多种验证规则
- 实现一个错误边界组件,捕获子组件的错误并显示备用 UI