Skip to content

《JavaScript全栈教程》10:错误处理——try-catch-finally与异步错误全解

约 2603 字大约 9 分钟

JavaScript全栈教程JavaScript

2026-04-12

本篇核心收获

  • 理解JavaScript中两种错误类型:逻辑错误与异常错误
  • 掌握try...catch...finally的三种语法形式及完整执行流程
  • 熟悉内置错误类型(Error、TypeError、ReferenceError)及如何区分处理
  • 学会使用throw主动抛出错误,并遵循最佳实践
  • 理解错误沿调用链向上传播的机制,并学会在合适位置统一捕获
  • 掌握异步代码(setTimeout、事件处理)中错误捕获的正确方式与修复技巧

一、错误处理概述:两种错误类型

在执行JavaScript代码时,有些情况下会发生错误。错误分为两种:

1. 逻辑错误(程序bug)

这类错误是由于程序逻辑不正确导致的代码执行异常。例如:

let s = null;
let len = s.length; // TypeError: null变量没有length属性

对于这种错误,需要修复程序本身,而不是用try...catch去“掩盖”。

2. 异常错误(不可预见的运行时异常)

执行过程中,程序可能遇到无法预测的异常情况而报错,例如:网络连接中断、读取不存在的文件、没有操作权限等。

对于这种错误,我们需要处理它,并可能需要给用户反馈。

背景知识:传统错误码方式(以C语言为例)

错误处理是程序设计时必须要考虑的问题。对于C这样贴近系统底层的语言,错误是通过错误码返回的:

int fd = open("/path/to/file", O_RDONLY);
if (fd == -1) {
    printf("Error when open file!");
} else {
    // Todo
}

通过错误码返回错误,就需要约定什么是正确的返回值,什么是错误的返回值。上面的open()函数约定返回-1表示错误。显然,这种用错误码表示错误在编写程序时十分不便。

因此,高级语言通常都提供了更抽象的错误处理逻辑try...catch...finally,JavaScript也不例外。

二、try...catch...finally 详解

2.1 基本语法与执行流程

使用try...catch...finally处理错误时,我们编写的代码如下:

let r1, r2, s = null;
try {
    r1 = s.length; // 此处应产生错误
    r2 = 100; // 该语句不会执行
} catch (e) {
    console.log('出错了: ' + e);
} finally {
    console.log('finally');
}
console.log('r1 = ' + r1); // r1应为undefined
console.log('r2 = ' + r2); // r2应为undefined

运行后可以发现,输出提示类似出错了:TypeError: Cannot read property 'length' of null

执行流程分析

当代码块被try { ... }包裹的时候,就表示这部分代码执行过程中可能会发生错误。

有错误发生时:

  1. 先执行try { ... }的代码;
  2. 执行到出错的语句时,后续语句不再继续执行,转而执行catch (e) { ... }代码;
  3. 最后执行finally { ... }代码。

没有错误发生时:

  1. 先执行try { ... }的代码;
  2. 因为没有出错,catch (e) { ... }代码不会被执行;
  3. 最后执行finally { ... }代码。

模块小结try块中的代码一旦出错,后续语句被跳过,直接进入catch;无论是否出错,finally都会执行。

2.2 try语句的三种形式

catchfinally可以不必都出现。也就是说,try语句一共有三种形式:

形式一:完整的try...catch...finally

try {
    // ...
} catch (e) {
    // ...
} finally {
    // ...
}

形式二:只有try...catch,没有finally

try {
    // ...
} catch (e) {
    // ...
}

形式三:只有try...finally,没有catch

try {
    // ...
} finally {
    // ...
}

三、错误类型(Error、TypeError等)

JavaScript有一个标准的Error对象表示错误,还有从Error派生的TypeErrorReferenceError等错误对象。

我们在处理错误时,可以通过catch(e)捕获的变量e访问错误对象,并根据不同错误类型做差异化处理:

try {
    // ...
} catch (e) {
    if (e instanceof TypeError) {
        alert('Type error!');
    } else if (e instanceof Error) {
        alert(e.message);
    } else {
        alert('Error: ' + e);
    }
}

注意:使用变量e是一个习惯用法,也可以以其他变量名命名,如catch(ex)

四、抛出错误(throw)

程序也可以主动抛出一个错误,让执行流程直接跳转到catch块。抛出错误使用throw语句。

示例:用户输入校验

下面的代码让用户输入一个数字,程序接收到的实际上是一个字符串,然后用parseInt()转换为整数。当用户输入不合法的时候,我们就抛出错误:

let r, n, s;
try {
    s = prompt('请输入一个数字');
    n = parseInt(s);
    if (isNaN(n)) {
        throw new Error('输入错误');
    }
    // 计算平方:
    r = n * n;
    console.log(n + '* ' + n + ' = ' + r);
} catch (e) {
    console.log('出错了:' + e);
}

最佳实践:实际上,JavaScript允许抛出任意对象,包括数字、字符串。但是,最好还是抛出一个Error对象

重要:catch块中必须编写错误处理语句

哪怕仅仅把错误打印出来,也不要什么也不干:

let n = 0, s;
try {
    n = s.length;
} catch (e) {
    console.log(e);
}
console.log(n);

错误示范(什么都不做):

let n = 0, s;
try {
    n = s.length;
} catch (e) {
    // 什么都不做
}
console.log(n);

因为catch到错误却什么都不执行,就不知道程序执行过程中到底有没有发生错误。

提示:处理错误时,请不要简单粗暴地用alert()把错误显示给用户。教程的代码使用alert()是为了便于演示。

五、错误传播机制

如果代码发生了错误,又没有被try...catch捕获,那么程序执行流程会跳转到哪里呢?

示例:未捕获的错误

function getLength(s) {
    return s.length;
}

function printLength() {
    console.log(getLength('abc')); // 3
    console.log(getLength(null)); // Error!
}

传播规则:如果在一个函数内部发生了错误,它自身没有捕获,错误就会被抛到外层调用函数,如果外层函数也没有捕获,该错误会一直沿着函数调用链向上抛出,直到被JavaScript引擎捕获,代码终止执行。

统一捕获的实践

我们不必在每一个函数内部捕获错误,只需要在合适的地方来个统一捕获,一网打尽:

function main(s) {
    console.log('BEGIN main()');
    try {
        foo(s);
    } catch (e) {
        console.log('出错了:' + e);
    }
    console.log('END main()');
}

function foo(s) {
    console.log('BEGIN foo()');
    bar(s);
    console.log('END foo()');
}

function bar(s) {
    console.log('BEGIN bar()');
    console.log('length = ' + s.length);
    console.log('END bar()');
}

main(null);

执行流程:当bar()函数传入参数null时,代码会报错,错误会向上抛给调用方foo()函数,foo()函数没有try...catch语句,所以错误继续向上抛给调用方main()函数,main()函数有try...catch语句,所以错误最终在main()函数被处理了。

至于在哪些地方捕获错误比较合适,需要视情况而定。

六、异步错误处理(重点难点)

6.1 异步执行的特点

编写JavaScript代码时,我们要时刻牢记:JavaScript引擎是一个事件驱动的执行引擎,代码总是以单线程执行,而回调函数的执行需要等到下一个满足条件的事件出现后,才会被执行。

例如,setTimeout()函数可以传入回调函数,并在指定若干毫秒后执行:

function printTime() {
    console.log('It is time!');
}

setTimeout(printTime, 1000);
console.log('done');

上面的代码会先打印done,1秒后才会打印It is time!

6.2 为什么try...catch无法捕获异步错误?

如果printTime()函数内部发生了错误,我们试图用try包裹setTimeout()无效的

function printTime() {
    throw new Error();
}

try {
    setTimeout(printTime, 1000);
    console.log('done');
} catch (e) {
    console.log('error');
}

原因:调用setTimeout()函数时,传入的printTime函数并未立刻执行!紧接着,JavaScript引擎会继续执行console.log('done');语句,而此时并没有错误发生。直到1秒钟后,执行printTime函数时才发生错误,但此时除了在printTime函数内部捕获错误外,外层代码并无法捕获。

结论:涉及到异步代码,无法在调用时捕获,原因就是在捕获的当时,回调函数并未执行。

类似的,当我们处理一个事件时,在绑定事件的代码处,无法捕获事件处理函数的错误

6.3 实战案例:表单计算器的错误处理修复

针对以下的表单:

<form>
    <input id="x"> + <input id="y">
    <button id="calc" type="button">计算</button>
</form>

我们用下面的代码给button绑定click事件(存在错误处理问题):

let btn = document.querySelector('#calc');

// 取消已绑定的事件:
btn.onclick = null;

btn.onclick = function () {
    let x = parseFloat(document.querySelector('#x').value);
    let y = parseFloat(document.querySelector('#y').value);
    let r;

    if (isNaN(x) || isNaN(y)) {
        throw new Error('输入有误');
    }

    r = x + y;
    alert('计算结果:' + r);
};

问题:用户输入错误时,处理函数内部抛出的错误无法被外部捕获。因为事件处理函数是异步执行的,外层代码无法使用try...catch捕获。

修复方案

将错误处理逻辑放在事件处理函数内部,使用try...catch包裹:

let btn = document.querySelector('#calc');

btn.onclick = function () {
    try {
        let x = parseFloat(document.querySelector('#x').value);
        let y = parseFloat(document.querySelector('#y').value);
        let r;

        if (isNaN(x) || isNaN(y)) {
            throw new Error('输入有误');
        }

        r = x + y;
        alert('计算结果:' + r);
    } catch (e) {
        // 在异步回调内部捕获并处理错误
        console.log('出错了:' + e.message);
        alert('请输入有效的数字'); // 友好的用户提示
    }
};

核心要点:对于异步代码(setTimeout、事件处理、Promise等),必须在异步回调函数内部进行错误捕获,而不是在外部包裹try...catch

本篇核心知识点速记

  1. 两种错误类型:逻辑错误(修复程序)和异常错误(需要捕获处理)。
  2. try-catch-finally执行流程:try块出错→跳转到catch→最后执行finally;无错→跳过catch→执行finally。
  3. 三种try形式:完整版、只有try-catch、只有try-finally。
  4. 错误类型判断:使用e instanceof TypeError等方式区分不同错误对象。
  5. 主动抛出错误:使用throw new Error('消息'),避免抛出非Error对象。
  6. catch块不能为空:至少要打印错误日志,否则无法得知程序是否出错。
  7. 错误传播链:错误沿调用链向上抛出,直到被捕获或引擎终止程序。可在顶层统一捕获。
  8. 异步错误捕获setTimeout、事件绑定等异步回调中的错误,必须在回调函数内部使用try-catch,外层无法捕获。