Skip to content

错误处理机制:优雅应对程序异常

在现实生活中,即使是最精心的计划也可能出现意外。飞机延误、服务器宕机、用户输入错误数据——这些都是不可避免的。优秀的程序不是不会出错,而是知道如何优雅地处理错误,让应用在遇到问题时依然能提供清晰的反馈,而不是直接崩溃。JavaScript 的 try-catch-finally 机制就是为此而生的安全网。

为什么需要错误处理

看看没有错误处理的代码会发生什么:

javascript
let user = null;

console.log("Starting app...");
let userName = user.name; // 💥 Cannot read property 'name' of null
console.log("This line will never run");

程序在第三行就崩溃了,后续的代码完全无法执行。用户看到的可能是一个空白页面或者神秘的错误信息,这是非常糟糕的用户体验。

使用错误处理,我们可以优雅地应对这种情况:

javascript
let user = null;

console.log("Starting app...");

try {
  let userName = user.name; // 尝试执行可能出错的代码
  console.log(`Welcome, ${userName}`);
} catch (error) {
  console.log("Unable to get user name. Using default.");
  let userName = "Guest";
  console.log(`Welcome, ${userName}`);
}

console.log("App continues running");

// 输出:
// Starting app...
// Unable to get user name. Using default.
// Welcome, Guest
// App continues running

程序没有崩溃,而是捕获了错误,执行备用逻辑,然后继续运行。这就是错误处理的价值。

Try-Catch 基本语法

try-catch 语句的结构很简单:

javascript
try {
  // 可能抛出错误的代码
} catch (error) {
  // 处理错误的代码
}

执行流程:

  1. JavaScript 先执行 try 块中的代码
  2. 如果没有错误,catch 块被跳过,继续执行后续代码
  3. 如果 try 块中发生错误,立即停止执行,跳转到 catch
  4. catch 块接收一个错误对象作为参数,包含错误信息
  5. 执行完 catch 块后,继续执行后续代码
javascript
console.log("Before try");

try {
  console.log("Try block - line 1");
  console.log("Try block - line 2");

  // 这里会抛出错误
  nonExistentFunction();

  console.log("Try block - line 4"); // 不会执行
} catch (error) {
  console.log("Catch block - error occurred");
  console.log("Error message:", error.message);
}

console.log("After try-catch");

// 输出:
// Before try
// Try block - line 1
// Try block - line 2
// Catch block - error occurred
// Error message: nonExistentFunction is not defined
// After try-catch

错误对象

catch 块接收的错误对象包含有用的信息:

javascript
try {
  let result = someUndefinedVariable + 10;
} catch (error) {
  console.log("Error name:", error.name); // ReferenceError
  console.log("Error message:", error.message); // someUndefinedVariable is not defined
  console.log("Error stack:", error.stack); // 调用栈跟踪
}

最常用的属性:

  • name:错误类型(如 ReferenceErrorTypeError
  • message:错误描述信息
  • stack:调用栈跟踪,用于调试

有时候你不需要错误对象,可以省略它(ES2019+):

javascript
try {
  riskyOperation();
} catch {
  // 不需要错误对象
  console.log("Something went wrong");
}

但大多数情况下,保留错误对象能提供更多有用的调试信息。

Finally 子句:无论如何都会执行

finally 块在 try-catch 之后执行,无论是否发生错误都会运行:

javascript
try {
  console.log("Try block");
  throw new Error("Oops!");
} catch (error) {
  console.log("Catch block");
} finally {
  console.log("Finally block");
}

// 输出:
// Try block
// Catch block
// Finally block

即使没有 catch 块,finally 也会执行:

javascript
function testFinally() {
  try {
    console.log("Try block");
    return "Returning from try";
  } finally {
    console.log("Finally block");
  }
}

console.log(testFinally());

// 输出:
// Try block
// Finally block
// Returning from try

注意 finallyreturn 之前执行!这使它成为清理资源的理想位置。

Finally 的典型应用

清理资源:

javascript
function readFile(filename) {
  let file = openFile(filename);

  try {
    return file.read();
  } catch (error) {
    console.error("Error reading file:", error.message);
    return null;
  } finally {
    file.close(); // 无论成功失败,都要关闭文件
  }
}

记录日志:

javascript
function processData(data) {
  let startTime = Date.now();

  try {
    // 处理数据
    return transform(data);
  } catch (error) {
    console.error("Processing failed:", error);
    throw error; // 重新抛出错误
  } finally {
    let duration = Date.now() - startTime;
    console.log(`Processing took ${duration}ms`);
  }
}

重置状态:

javascript
let isLoading = false;

function fetchData() {
  isLoading = true;

  try {
    let data = fetch("/api/data");
    return data;
  } catch (error) {
    console.error("Fetch failed:", error);
    return null;
  } finally {
    isLoading = false; // 无论成功失败,都要重置状态
  }
}

嵌套 Try-Catch

可以在 trycatchfinally 块中嵌套 try-catch

javascript
try {
  console.log("Outer try");

  try {
    console.log("Inner try");
    throw new Error("Inner error");
  } catch (innerError) {
    console.log("Inner catch:", innerError.message);
    throw new Error("Re-throwing from inner catch");
  }
} catch (outerError) {
  console.log("Outer catch:", outerError.message);
}

// 输出:
// Outer try
// Inner try
// Inner catch: Inner error
// Outer catch: Re-throwing from inner catch

这在处理多层操作时很有用:

javascript
function saveUserData(user) {
  try {
    // 验证用户数据
    try {
      validateUser(user);
    } catch (validationError) {
      console.error("Validation failed:", validationError.message);
      throw new Error("Invalid user data");
    }

    // 保存到数据库
    try {
      database.save(user);
    } catch (dbError) {
      console.error("Database error:", dbError.message);
      throw new Error("Failed to save user");
    }

    return { success: true };
  } catch (error) {
    console.error("Save operation failed:", error.message);
    return { success: false, error: error.message };
  }
}

不过,过深的嵌套会降低可读性。通常情况下,一到两层嵌套是合理的。

错误传播

如果在 catch 块中不处理错误,或者重新抛出错误(throw),错误会继续向上传播:

javascript
function inner() {
  throw new Error("Error from inner");
}

function middle() {
  try {
    inner();
  } catch (error) {
    console.log("Middle caught:", error.message);
    throw error; // 重新抛出
  }
}

function outer() {
  try {
    middle();
  } catch (error) {
    console.log("Outer caught:", error.message);
  }
}

outer();

// 输出:
// Middle caught: Error from inner
// Outer caught: Error from inner

如果一个 try-catch 没有捕获错误,或者内部有未处理的错误,它会继续向外层传播,直到被捕获或导致程序终止:

javascript
function riskyFunction() {
  throw new Error("Something bad happened");
}

function caller() {
  riskyFunction(); // 没有 try-catch
}

try {
  caller();
} catch (error) {
  console.log("Caught in outer try:", error.message);
}
// 输出: Caught in outer try: Something bad happened

实际应用场景

1. API 调用

javascript
async function fetchUserData(userId) {
  try {
    let response = await fetch(`/api/users/${userId}`);

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

    let data = await response.json();
    return { success: true, data };
  } catch (error) {
    console.error("Failed to fetch user:", error);
    return { success: false, error: error.message };
  }
}

// 使用
let result = await fetchUserData(123);
if (result.success) {
  console.log("User data:", result.data);
} else {
  console.log("Error:", result.error);
}

2. JSON 解析

javascript
function parseJSON(jsonString) {
  try {
    return JSON.parse(jsonString);
  } catch (error) {
    console.error("Invalid JSON:", error.message);
    return null;
  }
}

let data1 = parseJSON('{"name": "Alice"}'); // 有效
let data2 = parseJSON("invalid json"); // 无效

console.log(data1); // { name: "Alice" }
console.log(data2); // null

3. 用户输入验证

javascript
function calculateAge(birthYear) {
  try {
    let year = parseInt(birthYear);

    if (isNaN(year)) {
      throw new Error("Birth year must be a number");
    }

    if (year < 1900 || year > new Date().getFullYear()) {
      throw new Error("Birth year out of valid range");
    }

    return new Date().getFullYear() - year;
  } catch (error) {
    console.error("Invalid input:", error.message);
    return null;
  }
}

console.log(calculateAge("1990")); // 34 (假设当前是 2024)
console.log(calculateAge("invalid")); // Invalid input: Birth year must be a number
console.log(calculateAge("1800")); // Invalid input: Birth year out of valid range

4. 文件操作(Node.js)

javascript
const fs = require("fs");

function readConfig(filename) {
  try {
    let content = fs.readFileSync(filename, "utf8");
    let config = JSON.parse(content);
    return config;
  } catch (error) {
    if (error.code === "ENOENT") {
      console.error("Config file not found");
    } else if (error instanceof SyntaxError) {
      console.error("Config file has invalid JSON");
    } else {
      console.error("Error reading config:", error.message);
    }

    // 返回默认配置
    return { theme: "default", language: "en" };
  }
}

5. 数据库操作

javascript
async function createUser(userData) {
  let connection;

  try {
    connection = await database.connect();
    await connection.beginTransaction();

    // 创建用户
    let user = await connection.insert("users", userData);

    // 创建用户配置
    await connection.insert("settings", {
      userId: user.id,
      theme: "default",
    });

    await connection.commit();
    return { success: true, user };
  } catch (error) {
    if (connection) {
      await connection.rollback();
    }
    console.error("Failed to create user:", error);
    return { success: false, error: error.message };
  } finally {
    if (connection) {
      await connection.close();
    }
  }
}

异步错误处理

Promise 错误处理

Promise 有自己的错误处理机制:

javascript
fetch("/api/data")
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error("Error:", error));

但也可以结合 async/await 使用 try-catch

javascript
async function loadData() {
  try {
    let response = await fetch("/api/data");
    let data = await response.json();
    console.log(data);
  } catch (error) {
    console.error("Error:", error);
  }
}

处理多个异步操作

javascript
async function loadMultipleResources() {
  let results = {
    users: null,
    posts: null,
    comments: null,
  };

  try {
    results.users = await fetchUsers();
  } catch (error) {
    console.error("Failed to load users:", error);
  }

  try {
    results.posts = await fetchPosts();
  } catch (error) {
    console.error("Failed to load posts:", error);
  }

  try {
    results.comments = await fetchComments();
  } catch (error) {
    console.error("Failed to load comments:", error);
  }

  return results; // 即使部分失败,也返回已加载的数据
}

常见陷阱与最佳实践

1. 不要捕获所有错误后静默失败

javascript
// ❌ 糟糕:隐藏了错误
try {
  criticalOperation();
} catch (error) {
  // 什么都不做,错误被吞掉了
}

// ✅ 至少记录错误
try {
  criticalOperation();
} catch (error) {
  console.error("Critical operation failed:", error);
  // 根据情况决定是否重新抛出
}

2. 不要过度使用 Try-Catch

javascript
// ❌ 不需要的 try-catch
try {
  let sum = 1 + 2;
  console.log(sum);
} catch (error) {
  // 这段代码不可能出错
}

// ✅ 只在可能出错的地方使用
try {
  let data = JSON.parse(userInput); // 可能出错
  console.log(data);
} catch (error) {
  console.error("Invalid JSON");
}

try-catch 有轻微的性能开销,只应该用在真正可能抛出错误的地方。

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

javascript
// ❌ 没有上下文
catch (error) {
  console.log(error.message);
}

// ✅ 提供上下文
catch (error) {
  console.error(`Failed to process user ${userId}:`, error.message);
}

4. 区分可恢复和不可恢复的错误

javascript
try {
  let user = await fetchUser(id);

  if (!user) {
    // 可恢复:使用默认值
    user = { id, name: "Guest" };
  }

  // 不可恢复的错误会被 catch 捕获
} catch (error) {
  // 严重错误:通知用户并记录
  console.error("Fatal error:", error);
  showErrorMessage("Unable to load user data. Please try again later.");
}

5. Finally 与 Return

javascript
function test() {
  try {
    return "try";
  } finally {
    console.log("finally runs first");
  }
}

console.log(test());
// 输出:
// finally runs first
// try

// ⚠️ Finally 中的 return 会覆盖 try 中的 return
function覆盖Example() {
  try {
    return "try";
  } finally {
    return "finally"; // 这会覆盖 try 的返回值
  }
}

console.log(覆盖Example()); // finally

通常不应该在 finally 中使用 return,这会让代码难以理解。

总结

错误处理是编写健壮应用的关键。try-catch-finally 机制让你能够优雅地处理异常,确保程序在遇到问题时不会崩溃,而是提供清晰的反馈并继续运行。

关键要点:

  • try 块包含可能出错的代码
  • catch 块处理错误,接收错误对象
  • finally 块无论如何都会执行,适合清理资源
  • 不要捕获后静默失败,至少要记录错误
  • 只在真正可能出错的地方使用 try-catch
  • 提供有意义的错误信息和上下文
  • 区分可恢复和不可恢复的错误
  • 异步代码使用 async/await 配合 try-catch

良好的错误处理不是让程序不出错(这不可能),而是让程序在出错时表现得专业、优雅、用户友好。掌握错误处理机制,是成为专业开发者的重要一步。