Featured image of post JavaScript错误处理完全指南

JavaScript错误处理完全指南

本手册深入探讨JavaScript错误处理机制,涵盖错误类型、try-catch使用、自定义错误创建及实际应用场景,帮助开发者编写更健壮的代码。

JavaScript错误处理手册

错误和异常在应用程序开发中是不可避免的。作为程序员,我们有责任优雅地处理这些错误,以确保应用程序的用户体验不受影响。正确处理错误还有助于程序员调试和理解错误原因,从而更好地解决问题。

JavaScript作为一种流行的编程语言已有三十多年历史。我们使用JavaScript和各种基于JavaScript的流行库(如ReactJS)和框架(如Next.js、Remix等)构建Web、移动、PWA和服务器端应用程序。

作为一种弱类型语言,JavaScript带来了正确处理类型安全的挑战。TypeScript有助于管理类型,但我们仍然需要在代码中高效处理运行时错误。

如果您使用JavaScript构建应用程序一段时间,可能对TypeError、RangeError、ReferenceError等错误相当熟悉。所有这些错误都可能导致无效数据、不良页面转换、不需要的结果,甚至整个应用程序崩溃——这些都不会让最终用户满意!

在本手册中,您将学习关于JavaScript错误处理的一切知识。我们将从理解错误、错误类型和发生情况开始。然后您将学习如何处理这些错误,以免它们导致不良的用户体验。最后,您还将学习构建自己的自定义错误类型和清理方法,以更好地处理代码流以进行优化和性能提升。

JavaScript中的错误

错误和异常是破坏程序执行的事件。JavaScript逐行解析和执行代码。源代码会根据编程语言的语法进行评估,以确保其有效和可执行。如果存在不匹配,JavaScript会遇到解析错误。您需要确保遵循正确的语言语法和文法,以避免解析错误。

查看以下代码片段。这里,我们犯了没有关闭console.log括号的错误。

1
console.log("hi"

这将导致语法错误:

其他类型的错误可能由于错误的数据输入、尝试读取不存在的值或属性,或对不准确的数据进行操作而发生。让我们看一些例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
console.log(x); // Uncaught ReferenceError: x is not defined

let obj = null;
console.log(obj.name); // Uncaught TypeError: Cannot read properties of null

let arr = new Array(-1) // Uncaught RangeError: Invalid array length

decodeURIComponent("%"); // Uncaught URIError: URI malformed

eval("var x = ;"); // Uncaught EvalError

以下是您可能遇到的可能运行时错误及其描述列表:

  • ReferenceError – 当尝试访问未定义的变量时发生。
  • TypeError – 当对错误类型的值执行操作时发生。
  • RangeError – 当值超出允许范围时发生。
  • SyntaxError – 当JavaScript代码语法有错误时发生。
  • URIError – 当在编码和解码URI时使用了错误的URI函数时发生。
  • EvalError – 当eval()函数有问题时发生。
  • InternalError – 当JavaScript引擎遇到内部限制(堆栈溢出)时发生。
  • AggregateError – 在ES2021中引入,用于同时处理多个错误。
  • Custom Errors – 这些是用户定义的错误,我们很快将学习如何创建和使用它们。

您是否注意到我们上面使用的所有代码示例都会产生一条解释错误的消息?如果您仔细查看这些消息,会发现一个名为Uncaught的单词。这意味着错误发生了,但未被捕获和管理。这正是我们现在要解决的问题——让您知道如何处理这些错误。

使用try和catch处理错误

JavaScript应用程序可能因各种原因崩溃,如无效语法、无效数据、缺少API响应、用户错误等。这些原因大多可能导致应用程序崩溃,您的用户将看到一个空白的白页。

您可以使用try…catch优雅地处理这些情况,而不是让应用程序崩溃。

1
2
3
4
5
try {
    // 逻辑或代码
} catch (err) {
    // 处理错误
}

try块

try块包含可能抛出错误的代码——业务逻辑。开发人员总是希望他们的代码没有错误。但同时,您应该意识到代码可能因多种原因抛出错误,例如:

  • 解析JSON
  • 运行API逻辑
  • 访问嵌套对象属性
  • DOM操作
  • 以及更多

当try块中的代码抛出错误时,try块中剩余代码的执行将被暂停,控制权转移到最近的catch块。如果没有发生错误,catch块将被完全跳过。

1
2
3
4
5
try {
  // 可能抛出错误的代码
} catch (error) {
  // 在此处理错误
}

catch块

catch块仅在try块中抛出错误时运行。它接收Error对象作为参数,为我们提供有关错误的更多信息。在下面显示的示例中,我们使用了名为abc的东西而没有声明它。JavaScript将抛出这样的错误:

1
2
3
4
5
6
7
try {
    console.log("execution starts here");
    abc;
    console.log("execution ends here");
} catch (err) {
    console.error("An Error has occured", err);
}

JavaScript逐行执行代码。上述代码的执行顺序将是:

  1. 首先,字符串"execution starts here"将被记录到控制台。
  2. 然后控制权将移动到下一行并在那里找到abc。它是什么?JavaScript在任何地方都找不到它的定义。是时候发出警报并抛出错误了。控制权不会移动到下一行(下一个console log),而是移动到catch块。
  3. 在catch块中,我们通过将错误记录到控制台来处理错误。我们可以做许多其他事情,如显示toast消息、向用户发送电子邮件或关闭烤面包机(如果您的客户需要)。

如果没有try…catch,错误将使应用程序崩溃。

错误处理:实际用例

现在让我们看看使用try…catch进行错误处理的一些实际用例。

处理除以零

这是一个将一个数除以另一个数的函数。因此,我们为两个数传递了函数参数。我们希望确保除法永远不会遇到除以零(0)的错误。

作为主动措施,我们编写了一个条件:如果除数为零,我们将抛出一个错误,说明不允许除以零。在所有其他情况下,我们将继续进行除法运算。如果发生错误,catch块将处理错误并执行所需操作(在这种情况下,将错误记录到控制台)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function divideNumbers(a, b) {
    try {
        if (b === 0) {
            const err = new Error("Division by zero is not allowed.");
            throw err;
        }
        const result = a/b;
        console.log(`The result is ${result}`);
    } catch(error) {
        console.error("Got a Math Error:", error.message)
    }
}

现在,如果我们使用以下参数调用函数,我们将得到结果5,第二个参数是非零值。

1
divideNumbers(15, 3); // The result is 5

但如果我们为第二个参数传递0值,程序将抛出错误,并将其记录到控制台。

1
divideNumbers(15, 0);

输出:

处理JSON

通常,您会获得JSON作为API调用的响应。您需要在JavaScript代码中解析此JSON以提取值。如果API错误地发送了一些格式错误的JSON怎么办?您不能让用户界面因此崩溃。您需要优雅地处理它——这时try…catch块再次来救援:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function parseJSONSafely(str) {
  try {
    return JSON.parse(str);
  } catch (err) {
    console.error("Invalid JSON:", err.message);
    return null;
  }
}

const userData = parseJSONSafely('{"name": "tapaScript"}'); // 已解析
const badData = parseJSONSafely('name: tapaScript');         // 优雅处理

如果没有try…catch,第二次调用将使应用程序崩溃。

Error对象的剖析

在编程中遇到错误可能是一种可怕的感觉。但JavaScript中的错误不仅仅是一些可怕、烦人的消息——它们是结构化的对象,携带了大量有关出错原因、位置和方式的有用信息。

作为开发人员,我们需要理解Error对象的剖析,以帮助我们在生产级应用程序问题中更快地调试和更智能地恢复。

让我们通过示例深入探讨Error对象、其属性以及如何有效使用它。

什么是Error对象?

当运行时出现问题时,JavaScript引擎会抛出一个Error对象。此对象包含有用的信息,如:

  • 错误消息:这是人类可读的错误消息。
  • 错误类型:TypeError、ReferenceError、SyntaxError等,我们上面讨论过。
  • 堆栈跟踪:这帮助您导航到错误的根源。它是一个包含错误抛出点堆栈跟踪的字符串。

让我们看一下下面的代码片段。JavaScript引擎将在此代码中抛出错误,因为变量y未定义。错误对象包含错误名称(类型)、消息和堆栈跟踪信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function doSomething() {
  const x = y + 1; // y未定义
}
try {
  doSomething();
} catch (err) {
  console.log(err.name);    // ReferenceError
  console.log(err.message); // y is not defined
  console.log(err.stack); // ReferenceError: y is not defined
                          // at doSomething (<anonymous>:2:13)
                          // at <anonymous>:5:3
}

提示:如果您需要错误对象中的任何特定属性,可以使用解构来实现。以下是一个示例,我们只对错误名称和消息感兴趣,而不是堆栈。

1
2
3
4
5
6
try {
  JSON.parse("{ invalid json }");
} catch ({name, message}) {
  console.log("Name:", name);       // Name: SyntaxError
  console.log("Message:", message); // Message: Expected property name or '}' in JSON at position 2 (line 1 column 3)
}

抛出错误和重新抛出错误

JavaScript提供了一个throw语句来手动触发错误。当您想在代码中处理无效条件时,这非常有用(还记得除以零的问题吗?)。

要抛出错误,您需要创建Error对象的实例并附带详细信息,然后抛出它。

1
throw new Error("Something is bad!");

当代码执行遇到throw语句时:

  • 它立即停止当前代码块的执行。
  • 控制权转移到最近的catch块(如果有)。
  • 如果未找到catch块,错误将不会被捕获。错误会冒泡,并可能最终使程序崩溃。

重新抛出

有时,在catch块中捕获错误本身是不够的。有时,您可能不知道如何完全处理错误,并且可能想做其他事情,例如:

  • 向错误添加上下文。
  • 将错误记录到基于文件的记录器中。
  • 将错误传递给更专业的人来处理。

这就是重新抛出的用武之地。通过重新抛出,您可以捕获错误,对其进行其他操作,然后再次抛出它。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function processData() {
  try {
    parseUserData();
  } catch (err) {
    console.error("Error in processData:", err.message);
    throw err; // 重新抛出,以便外部函数也可以处理它
  }
}

function main() {
  try {
    processData();
  } catch (err) {
    handleErrorBetter(err);
  }
}

在上面的代码中,processData()函数捕获错误,记录它,然后再次抛出它。外部main()函数现在可以捕获它并做更多事情以更好地处理它。

在真实的应用程序开发中,您会希望分离错误的关注点,例如:

  • API层 – 在此层中,您可以检测HTTP失败

    1
    2
    3
    4
    5
    
    async function fetchUser(id) {
      const res = await fetch(`/users/${id}`);
      if (!res.ok) throw new Error("User not found"); // 在此抛出
      return res.json();
    }
    
  • 服务层 – 在此层中,您处理业务逻辑。因此,将处理无效条件的错误。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    async function getUser(id) {
      try {
        const user = await fetchUser(id);
        return user;
      } catch (err) {
        console.error("Fetching user failed:", err.message);
        throw new Error("Unable to load user profile"); // 重新抛出
      }
    }
    
  • UI层 – 显示用户友好的错误消息。

    1
    2
    3
    4
    5
    6
    7
    8
    
    async function showUserProfile() {
      try {
        const user = await getUser(123);
        renderUser(user);
      } catch (err) {
        displayError(err.message); // 向用户显示适当消息
      }
    }
    

带有try-catch的finally

try…catch块为我们提供了一种优雅处理错误的方法。但您可能总是希望执行一些代码,无论是否发生错误。例如,关闭数据库连接、停止加载器、重置某些状态等。这就是finally的用武之地。

1
2
3
4
5
6
7
try {
  // 可能抛出错误的代码
} catch (error) {
  // 处理错误
} finally {
  // 总是运行,无论是否发生错误
}

让我们举个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function performTask() {
  try {
    console.log("Doing something cool...");
    throw new Error("Oops!");
  } catch (err) {
    console.error("Caught error:", err.message);
  } finally {
    console.log("Cleanup: Task finished (success or fail).");
  }
}

performTask();

在performTask()函数中,错误在第一个console log之后抛出。因此,控制权将移动到catch块并记录错误。之后,finally块将执行其console log。

输出:

1
2
3
Doing something cool...
Caught error: Oops!
Cleanup: Task finished (success or fail).

让我们看一个更真实的用例,即进行API调用和处理加载微调器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
async function loadUserData() {
  showSpinner(); // 在此显示微调器

  try {
    const res = await fetch('/api/user');
    const data = await res.json();
    displayUser(data);
  } catch (err) {
    showError("Failed to load user.");
  } finally {
    hideSpinner(); // 在成功和失败情况下都隐藏微调器。
  }
}

通常,在从浏览器进行API(异步)调用时,我们会显示加载微调器。无论API调用是成功响应还是错误,我们都必须停止显示加载微调器。您可以在finally块中执行此操作,而不是执行两次代码逻辑来停止微调器(一次在try块内部,然后在catch块内部再次执行)。

使用finally的注意事项

finally块可以覆盖返回值或抛出的错误。这种行为可能会令人困惑,并可能导致错误。

1
2
3
4
5
6
7
8
9
function test() {
  try {
    return 'from try';
  } finally {
    return 'from finally';
  }
}

console.log(test());

您认为上面的代码返回什么? 它将返回’from finally’。return ‘from try’被完全忽略。finally中的返回默默地覆盖了它。

让我们再看一个相同问题的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function willThrow() {
  try {
    throw new Error("Original Error");
  } finally {
    throw new Error("Overriding Error"); // 原始错误丢失
  }
}

try {
  willThrow();
} catch (err) {
  console.log(err.message); // "Overriding Error"
}

这里,原始错误(“Original Error”)被吞没了。finally块覆盖了实际的根本原因。

使用finally时:

  • 尽可能避免从finally返回和抛出。
  • 避免在finally块中执行可能影响实际结果的逻辑。try块是处理此问题的最佳位置。
  • 必须避免在finally块内进行任何关键决策。
  • 使用finally进行清理活动,例如关闭文件、连接和停止加载微调器等。
  • 确保finally块包含无副作用的代码。

自定义错误

在复杂的应用程序中,使用通用Error及其现有类型(如ReferenceError、SyntaxError等)可能有点模糊。JavaScript允许您创建与业务用例更相关的自定义错误。自定义错误可以提供:

  • 有关错误的额外上下文信息。
  • 错误的清晰度。
  • 更可读的日志。
  • 有条件处理多个错误情况的能力。

JavaScript中的自定义错误是用户定义的错误类型,扩展了内置的Error类。自定义错误应该是一个ES6类,扩展JavaScript的Error类。我们可以在构造函数中使用super()来继承Error类的message属性。您可以选择为自定义错误分配名称并清理堆栈跟踪。

1
2
3
4
5
6
7
class MyCustomError extends Error {
  constructor(message) {
    super(message);         // 继承message属性
    this.name = this.constructor.name; // 可选但推荐
    Error.captureStackTrace(this, this.constructor); // 清理堆栈跟踪
  }
}

现在让我们看一个自定义错误的真实用例。

自定义错误的真实用例

在网页上使用表单是一个极其常见的用例。表单可能包含一个或多个输入字段。建议在处理表单数据进行任何服务器操作之前验证用户输入。

让我们创建一个自定义验证错误,可用于验证多个表单输入数据,如用户的电子邮件、年龄、电话号码等。

首先,我们将创建一个名为ValidationError的类,扩展Error类。构造函数使用错误消息设置ValidationError类。我们还可以实例化其他属性,如name、field等。

1
2
3
4
5
6
7
class ValidationError extends Error {
  constructor(field, message) {
    super(`${field}: ${message}`);
    this.name = "ValidationError";
    this.field = field;
  }
}

现在,让我们看看如何使用ValidationError。我们可以验证用户模型以检查其属性,并在期望不匹配时抛出ValidationError。

1
2
3
4
5
6
7
8
function validateUser(user) {
  if (!user.email.includes("@")) {
    throw new ValidationError("email", "Invalid email format");
  }
  if (!user.age || user.age < 18) {
    throw new ValidationError("age", "User must be 18+");
  }
}

在上面的代码片段中:

  • 如果用户的电子邮件不包含@符号,我们抛出无效电子邮件格式验证错误。
  • 如果用户的年龄信息缺失或低于18岁,我们抛出另一个验证错误。

自定义错误使我们能够创建特定于领域/用途的错误类型,以保持代码更易于管理且更少出错。

给您的任务分配

如果您已经阅读了本手册,我希望您现在对JavaScript错误处理有了扎实的理解。让我们根据到目前为止学到的知识尝试一些任务。这会很有趣。

找出输出

以下代码片段的输出是什么?为什么?

1
2
3
4
5
6
try {
    let r = p + 50;
    console.log(r);
} catch (error) {
    console.log("An error occurred:", error.name);
}

选项是:

  • ReferenceError
  • SyntaxError
  • TypeError
  • 无错误,打印10

支付流程验证

编写一个函数processPayment(amount),验证金额是否为正数且不超过余额。如果任何条件失败,抛出适当的错误。

提示:您可以考虑在此创建自定义错误。

40天JavaScript挑战倡议

学习某件事有101种方法。但没有什么能比得上结构化和渐进式的学习方法。在软件工程领域工作了二十多年后,我能够收集最好的JavaScript内容,创建了40天JavaScript挑战倡议。

如果您想通过基本概念、项目和任务免费(永远)学习JavaScript,请查看它。专注于JavaScript的基础知识将为您在Web开发的未来做好充分准备。

结束之前…

就这样!我希望您觉得这篇文章有见地。

让我们联系:

  • 订阅我的YouTube频道。
  • 如果您不想错过每日技能提升技巧,请在
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计