Skip to content

抛出自定义错误:精确的错误控制

当一位厨师发现食材已经过期,他会立即停止烹饪并发出警告:"这些食材不能使用!"而不是继续制作可能导致食物中毒的菜肴。在编程中,throw 语句就像这样的警告机制——当代码检测到无法继续执行的条件时,主动抛出错误,让调用者知道出了问题,而不是默默产生错误的结果。

Throw 语句:主动抛出错误

JavaScript 会自动抛出很多类型的错误(如访问 undefined 的属性),但有时你需要根据业务逻辑主动抛出错误。这就是 throw 语句的作用:

javascript
function divide(a, b) {
  if (b === 0) {
    throw "Cannot divide by zero!"; // 抛出错误
  }

  return a / b;
}

try {
  let result = divide(10, 0);
  console.log(result);
} catch (error) {
  console.log("Error occurred:", error);
}

// 输出: Error occurred: Cannot divide by zero!

throw 语句会立即终止当前函数的执行,并将控制权交给最近的 catch 块。如果没有 catch 块,程序会崩溃。

技术上,你可以抛出任何类型的值:

javascript
throw "This is a string error";
throw 42;
throw true;
throw { message: "Custom object", code: 404 };
throw new Error("This is an Error object");

但在实践中,应该总是抛出 Error 对象或其子类,原因我们稍后会解释。

Error 对象:标准的错误表示

JavaScript 提供了内置的 Error 构造函数来创建标准错误对象:

javascript
throw new Error("Something went wrong");

Error 对象有几个重要属性:

javascript
let error = new Error("File not found");

console.log(error.name); // "Error"
console.log(error.message); // "File not found"
console.log(error.stack); // 调用栈信息

使用 Error 对象的好处:

  1. 标准化:所有错误都有相同的接口(name、message、stack)
  2. 堆栈跟踪:自动包含调用栈信息,方便调试
  3. 类型检查:可以使用 instanceof 判断错误类型
  4. 工具支持:调试工具和日志系统更好地识别 Error 对象
javascript
function readFile(filename) {
  if (!filename) {
    throw new Error("Filename is required");
  }

  if (!fileExists(filename)) {
    throw new Error(`File not found: ${filename}`);
  }

  // 读取文件...
}

try {
  readFile("");
} catch (error) {
  console.log(error.name); // Error
  console.log(error.message); // Filename is required
  console.log(error.stack); // 完整的调用栈
}

创建自定义错误类

虽然 Error 对象很有用,但有时你需要更 specific 的错误类型,以便区分不同类别的错误。可以通过继承 Error 类来创建自定义错误:

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

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

// 使用自定义错误
function validateEmail(email) {
  if (!email) {
    throw new ValidationError("Email is required");
  }

  if (!email.includes("@")) {
    throw new ValidationError("Invalid email format");
  }

  return true;
}

function fetchData(url) {
  // 模拟网络请求
  if (url === "") {
    throw new NetworkError("URL cannot be empty", 400);
  }

  // 模拟服务器错误
  throw new NetworkError("Server unavailable", 503);
}

现在可以根据错误类型采取不同的处理策略:

javascript
try {
  validateEmail("invalid-email");
} catch (error) {
  if (error instanceof ValidationError) {
    console.log("Validation failed:", error.message);
    // 显示用户友好的提示
  } else if (error instanceof NetworkError) {
    console.log("Network error:", error.message, "Status:", error.statusCode);
    // 重试或显示网络错误提示
  } else {
    console.log("Unknown error:", error);
    // 处理其他错误
  }
}

包含更多上下文信息

自定义错误类可以包含任何对调试有用的信息:

javascript
class DatabaseError extends Error {
  constructor(message, query, params) {
    super(message);
    this.name = "DatabaseError";
    this.query = query;
    this.params = params;
    this.timestamp = new Date();
  }

  toString() {
    return `${this.name}: ${this.message}
    Query: ${this.query}
    Params: ${JSON.stringify(this.params)}
    Time: ${this.timestamp.toISOString()}`;
  }
}

function executeQuery(query, params) {
  try {
    // 模拟数据库查询
    throw new Error("Connection timeout");
  } catch (error) {
    throw new DatabaseError("Query execution failed", query, params);
  }
}

try {
  executeQuery("SELECT * FROM users WHERE id = ?", [123]);
} catch (error) {
  if (error instanceof DatabaseError) {
    console.log(error.toString());
    // 记录详细的错误信息到日志
  }
}

实际应用场景

1. 输入验证

javascript
class InvalidInputError extends Error {
  constructor(field, value, reason) {
    super(`Invalid ${field}: ${reason}`);
    this.name = "InvalidInputError";
    this.field = field;
    this.value = value;
    this.reason = reason;
  }
}

function createUser(userData) {
  // 验证用户名
  if (!userData.username || userData.username.length < 3) {
    throw new InvalidInputError(
      "username",
      userData.username,
      "Username must be at least 3 characters"
    );
  }

  // 验证年龄
  if (userData.age < 18 || userData.age > 120) {
    throw new InvalidInputError(
      "age",
      userData.age,
      "Age must be between 18 and 120"
    );
  }

  // 验证邮箱
  if (!userData.email || !userData.email.includes("@")) {
    throw new InvalidInputError(
      "email",
      userData.email,
      "Invalid email format"
    );
  }

  // 创建用户...
  return { id: 1, ...userData };
}

try {
  let user = createUser({
    username: "ab",
    age: 25,
    email: "[email protected]",
  });
  console.log("User created:", user);
} catch (error) {
  if (error instanceof InvalidInputError) {
    console.log(`Field ${error.field} is invalid: ${error.reason}`);
    // 显示表单错误提示
  }
}

2. API 错误处理

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

class NotFoundError extends APIError {
  constructor(endpoint) {
    super("Resource not found", 404, endpoint);
    this.name = "NotFoundError";
  }
}

class UnauthorizedError extends APIError {
  constructor(endpoint) {
    super("Unauthorized access", 401, endpoint);
    this.name = "UnauthorizedError";
  }
}

async function apiRequest(endpoint, token) {
  if (!token) {
    throw new UnauthorizedError(endpoint);
  }

  // 模拟 API 调用
  let response = { status: 404 };

  if (response.status === 404) {
    throw new NotFoundError(endpoint);
  }

  if (response.status === 401) {
    throw new UnauthorizedError(endpoint);
  }

  if (response.status >= 400) {
    throw new APIError("Request failed", response.status, endpoint);
  }

  return response.data;
}

try {
  let data = await apiRequest("/api/users/123", null);
} catch (error) {
  if (error instanceof UnauthorizedError) {
    console.log("Please log in");
    // 重定向到登录页
  } else if (error instanceof NotFoundError) {
    console.log("Resource not found:", error.endpoint);
    // 显示 404 页面
  } else if (error instanceof APIError) {
    console.log(`API error (${error.statusCode}):`, error.message);
    // 显示错误提示
  }
}

3. 业务逻辑错误

javascript
class InsufficientFundsError extends Error {
  constructor(balance, amount) {
    super(`Insufficient funds: balance ${balance}, attempted ${amount}`);
    this.name = "InsufficientFundsError";
    this.balance = balance;
    this.amount = amount;
  }
}

class AccountLockedError extends Error {
  constructor(accountId, reason) {
    super(`Account ${accountId} is locked: ${reason}`);
    this.name = "AccountLockedError";
    this.accountId = accountId;
    this.reason = reason;
  }
}

function withdraw(account, amount) {
  if (account.locked) {
    throw new AccountLockedError(account.id, "Too many failed login attempts");
  }

  if (amount > account.balance) {
    throw new InsufficientFundsError(account.balance, amount);
  }

  if (amount <= 0) {
    throw new Error("Amount must be positive");
  }

  account.balance -= amount;
  return { newBalance: account.balance, amount };
}

let account = { id: "123", balance: 100, locked: false };

try {
  let result = withdraw(account, 150);
  console.log("Withdrawal successful:", result);
} catch (error) {
  if (error instanceof InsufficientFundsError) {
    console.log(
      `Cannot withdraw $${error.amount}. Current balance: $${error.balance}`
    );
    // 显示余额不足提示
  } else if (error instanceof AccountLockedError) {
    console.log(`Account locked: ${error.reason}`);
    // 显示账户锁定信息
  } else {
    console.log("Error:", error.message);
  }
}

4. 配置和初始化错误

javascript
class ConfigurationError extends Error {
  constructor(key, value, expected) {
    super(`Invalid configuration for ${key}`);
    this.name = "ConfigurationError";
    this.key = key;
    this.value = value;
    this.expected = expected;
  }
}

function loadConfiguration(config) {
  if (!config.apiUrl) {
    throw new ConfigurationError("apiUrl", undefined, "Valid URL string");
  }

  if (typeof config.timeout !== "number" || config.timeout <= 0) {
    throw new ConfigurationError(
      "timeout",
      config.timeout,
      "Positive number (milliseconds)"
    );
  }

  if (!["development", "production"].includes(config.environment)) {
    throw new ConfigurationError(
      "environment",
      config.environment,
      "'development' or 'production'"
    );
  }

  return config;
}

try {
  let config = loadConfiguration({
    apiUrl: "https://api.example.com",
    timeout: -1000,
    environment: "staging",
  });
} catch (error) {
  if (error instanceof ConfigurationError) {
    console.error(`Configuration error: ${error.key}`);
    console.error(`  Current value: ${error.value}`);
    console.error(`  Expected: ${error.expected}`);
  }
}

错误重新抛出

有时你需要捕获错误,执行某些操作(如日志记录),然后重新抛出以便上层处理:

javascript
function processData(data) {
  try {
    return transform(data);
  } catch (error) {
    console.error("Transform failed at:", new Date());
    console.error("Input data:", data);

    throw error; // 重新抛出原始错误
  }
}

try {
  processData(invalidData);
} catch (error) {
  console.log("Caught in outer try:", error.message);
}

也可以包装错误,提供更多上下文:

javascript
function loadUserData(userId) {
  try {
    let data = fetchFromDatabase(userId);
    return parseUserData(data);
  } catch (error) {
    // 包装原始错误,提供更多信息
    throw new Error(`Failed to load user ${userId}: ${error.message}`);
  }
}

或者创建新的自定义错误,保留原始错误:

javascript
class DataProcessingError extends Error {
  constructor(message, cause) {
    super(message);
    this.name = "DataProcessingError";
    this.cause = cause; // 保存原始错误
  }
}

function processFile(filename) {
  try {
    let content = readFile(filename);
    return parseContent(content);
  } catch (error) {
    throw new DataProcessingError(`Failed to process file: ${filename}`, error);
  }
}

try {
  processFile("data.json");
} catch (error) {
  console.log(error.message); // Failed to process file: data.json
  console.log("Original error:", error.cause.message); // 原始错误信息
}

条件性抛出错误

并非所有错误情况都需要抛出错误。有时返回特殊值或使用默认值更合适:

javascript
// 抛出错误:严格模式
function getUser(id) {
  let user = database.findUser(id);

  if (!user) {
    throw new Error(`User ${id} not found`); // 调用者必须处理
  }

  return user;
}

// 返回 null:宽松模式
function getUserOrNull(id) {
  let user = database.findUser(id);
  return user || null; // 调用者可以简单检查
}

// 混合策略
function getUser(id, options = {}) {
  let user = database.findUser(id);

  if (!user) {
    if (options.throwOnNotFound) {
      throw new Error(`User ${id} not found`);
    }
    return null;
  }

  return user;
}

选择哪种方式取决于:

  • 抛出错误:当缺少数据是严重的、意外的情况
  • 返回 null/undefined:当缺少数据是正常的、预期的情况
  • 返回默认值:当可以提供合理的 fallback

常见陷阱与最佳实践

1. 始终使用 Error 对象

javascript
// ❌ 抛出字符串
throw "Something went wrong";

// ❌ 抛出普通对象
throw { message: "Error", code: 500 };

// ✅ 使用 Error 对象
throw new Error("Something went wrong");

// ✅ 或自定义 Error 类
throw new CustomError("Something went wrong");

2. 提供描述性的错误信息

javascript
// ❌ 模糊的错误信息
throw new Error("Invalid input");

// ✅ 具体的错误信息
throw new Error("Email address must contain @ symbol");

// ✅ 包含上下文
throw new Error(`User ${userId} not found in database`);

3. 不要吞掉错误

javascript
// ❌ 捕获后什么都不做
try {
  riskyOperation();
} catch (error) {
  // 错误被吞掉了
}

// ✅ 至少记录错误
try {
  riskyOperation();
} catch (error) {
  console.error("Operation failed:", error);
}

// ✅ 或重新抛出
try {
  riskyOperation();
} catch (error) {
  console.error("Operation failed:", error);
  throw error;
}

4. 使用有意义的错误名称

javascript
class UserNotFoundError extends Error {
  constructor(userId) {
    super(`User ${userId} not found`);
    this.name = "UserNotFoundError"; // 清晰的错误名称
    this.userId = userId;
  }
}

5. 不要过度使用自定义错误

javascript
// ❌ 为每个小错误创建类
class UsernameEmptyError extends Error {}
class UsernameTooShortError extends Error {}
class UsernameHasSpacesError extends Error {}
class UsernameHasSpecialCharsError extends Error {}

// ✅ 使用一个通用类,通过属性区分
class ValidationError extends Error {
  constructor(field, reason) {
    super(`${field} validation failed: ${reason}`);
    this.name = "ValidationError";
    this.field = field;
    this.reason = reason;
  }
}

throw new ValidationError("username", "cannot be empty");
throw new ValidationError("username", "must be at least 3 characters");

总结

主动抛出和自定义错误是构建健壮应用的重要技术。通过 throw 语句,你可以在检测到问题时立即停止执行;通过自定义 Error 类,你可以创建具有丰富上下文的错误对象,让错误处理更加精确和易于调试。

关键要点:

  • 使用 throw 语句主动抛出错误
  • 始终抛出 Error 对象或其子类,不要抛出字符串或普通对象
  • 创建自定义 Error 类来表示特定类型的错误
  • 在错误对象中包含有用的上下文信息
  • 提供清晰、描述性的错误消息
  • 根据情况选择抛出错误或返回默认值
  • 可以捕获、记录然后重新抛出错误
  • 不要过度使用自定义错误类,保持合理的粒度

良好的错误设计能让代码更易维护,调试更轻松,用户体验更友好。花时间设计好错误处理策略,是对代码质量的重要投资。