# JavaScript教程 - 18 异常处理

# 18.1 异常

JavaScript 已经是比较”宽容“的语言了,有一些情况,搁其他语言早就报错了,但是 JavaScript 还能运行,因为 JavaScript 在设计的时候,设计思想就是能不报错就不报错。

但是也不是所有情况 JavaScript 都不报错,有些时候程序确实是无法继续执行的,不得不报错并终止执行,我们可以对这些可能出现的错误情况进行预处理,防止程序崩溃或者给用户一些提示,这样可以增强代码的健壮性和提升用户体验。


举个栗子,有下面的代码,执行的时候会报错:

let obj = null;
console.log(obj.name);
1
2

上面的代码会报错,错误会在浏览器控制台打印:

# 18.2 异常处理

异常处理的语法结构:

try {
  // 可能出错的代码
} catch (error) {
  // 捕获异常后的处理逻辑
} finally {
  // (可选)无论是否出错都会执行
}
1
2
3
4
5
6
7
  • 将可能报错的代码放到 try 块中;
  • 如果代码运行出现异常,会被 catch 块捕获,并执行 catch 块中的代码;
  • 无论代码是否出错,finally 块的代码都会执行,finally 块可以省略。

举个栗子:

try {
  // 可能出错的代码
  let obj = null;
  console.log(obj.name); // TypeError
  
  console.log(123);  // 无法执行
} catch (error) {
  console.log("出错了:" + error);
} finally {
  console.log("总会执行的代码,如清理资源");
}

console.log(456);  // 可以执行
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 当 try 块中的代码出现异常,那么 try 块中出现异常的代码之后的代码,是无法执行的,所以上面是无法打印 123 的。
  • 出现异常,会被 catch 块捕获,异常信息会被封装到 error (名称可自定义)对象中。
  • 无论是否出现异常,finally 块的代码都会执行,如果需要,可以添加 finally 块,不需要可以省略;
  • 因为异常被捕获了,所以之后的代码是可以继续执行的,所以会打印 456
  • 注意:非常不建议 catch 块中什么代码也不写,这样出现异常,程序没有成功执行,还没有任何日志,不容易定位错误。所以最起码打印一下日志。

上面的代码执行结果如下:

出错了:Cannot read properties of null (reading 'name')
总会执行的代码,如清理资源
456
1
2
3

# 18.3 打印错误信息

错误信息会被封装到异常对象中,可以通过如下信息打印异常信息:

try {
  console.log(obj.name); // TypeError
} catch (error) {
  console.log(error);  // 1. 打印完整信息
  console.error(error);  // 2. 打印完整信息,红色更显眼
  console.log("出错了:" + error)  // 3. 打印简短的信息,包括类型和错误信息
  console.log(e.message); // 4. 只输出错误信息
  console.log(e.name);  // 5. 打印错误类型
  console.log(e.stack);  // 6. 打印调用栈信息
}

// ----打印信息如下:
// 1
ReferenceError: obj is not defined
    at index.html:10:21

// 2
index.html:13 ReferenceError: obj is not defined
    at index.html:10:21
(匿名)	@	index.html:13

// 3
出错了:ReferenceError: obj is not defined

// 4
出错了:obj is not defined

// 1
ReferenceError: obj is not defined
    at index.html:10:21

// 2
index.html:13 ReferenceError: obj is not defined
    at index.html:10:21
(匿名) @ index.html:13

// 3
出错了:ReferenceError: obj is not defined

// 4
obj is not defined

// 5
ReferenceError

// 6
ReferenceError: obj is not defined
    at index.html:10:21
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
  • 可以根据需要打印,开发时可以打印详细一些,将堆栈信息打印出来,例如查找错误。

# 18.4 异常的传递

什么是异常的传递?

异常的传递,就是当方法执行的时候出现异常,如果没有进行捕获,就会将异常传递给该方法的调用者,如果调用者仍未处理异常,则继续向上传递,直到传递给主程序,如果主程序仍然没有进行异常处理,则程序将被终止。

举个栗子:

function nullError() {
  let obj = null;
  console.log(obj.name);  // 会出错的代码
}

function testError() {
  nullError();  // 调用会抛出异常
}

try {
  testError();  // 这里会抛出异常
} catch (error) {
  console.log(error);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 上面代码,执行 nullError() 会抛出空指针异常,因为 nullError() 函数中没有进行异常处理,则异常会抛给 testError() 函数,testError() 没有进行异常处理,会继续向上抛给调用者,如果有调用者对异常进行了处理,程序不会崩溃。

利用异常的传递性,如果我们在程序运行的开始位置进行了异常捕获,无论程序哪里发生了错误,最终都会被传递到一开始调用的位置,就像上面那样,这样可以保证所有的异常都会被捕获。但是不要所有的异常都在这里处理,这只是兜底处理,至于异常在哪里地方处理,还是根据情况来确定。

# 18.5 主动抛出异常

有时候我们可以根据需要,主动抛出异常。

举个栗子:

function divide(a, b) {
  if (b === 0) {
    throw new Error("除数不能为 0");  // 主动抛出异常
  }
  return a / b;
}

try {
  let result = divide(5, 0);
  console.log("结果是:" + result);
} catch (e) {
  console.log("捕获异常:" + e.message);
  alert('计算出错,请检查数据是否正确');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 上面的代码中,divide 函数在除数为 0 的时候,主动抛出异常;
  • 调用的地方,可以进行异常捕获,并对异常进行处理。例如可以提示用户等处理。

# 18.6 异步函数的异常

需要注意:try...catch 只能捕获同步代码中的异常。

看一下下面的代码:

function test() {
  throw new Error();  // 抛出异常
}

try {
  setTimeout(test, 1000);
  console.log("done");
} catch (e) {
  console.log("error:" + e.message);
}
1
2
3
4
5
6
7
8
9
10
  • 上面的 try...catch 并不能捕获 test() 函数的异常,test() 函数并不是立即被执行,当 test() 函数被执行的时候,主程序已经执行完成了。

如果要捕获只能在回调函数中进行捕获:

function test() {
    try {
        throw new Error();
    } catch (e) {
        console.log('error:', e.message);
    }
}
1
2
3
4
5
6
7

# 18.7 自定义异常

JavaScript 内置了一些异常类型,常见的如下:

异常类型 继承自 说明 示例
Error 所有错误的基类 throw new Error("通用错误")
TypeError Error 类型错误,如方法调用、访问属性时类型不匹配 null.name123.toUpperCase()
ReferenceError Error 引用不存在的变量 console.log(a)(未定义)
SyntaxError Error 代码语法错误,如 eval('foo bar') JSON.parse("{name: 'Tom'}")
RangeError Error 数值超出范围,如递归太深、数组长度非法 new Array(-1)
URIError Error URI 格式错误,如 decodeURIComponent 无效 decodeURIComponent('%')
EvalError Error eval() 使用错误(很少见) 特殊场景

有时候程序有问题,可能会自动抛出上面的一些异常,我们也可以手动创建这些异常对象,并抛出:

throw new Error("除数不能为 0");

// 也可以创建其他的异常对象
throw new URIError("URI格式错误,请检查");
1
2
3
4

在捕获的时候,也可以通过错误对象,判断是什么类型的异常:

try {
  // ...
} catch (error) {
  console.log("出错了:" + error.message);

  // 判断异常类型
  if (error instanceof TypeError) {
    console.log("类型错误!");
  } else if (error instanceof ReferenceError) {
    console.log("引用错误!");
  } else {
    console.log("其他错误");
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在进行开发的时候,我们可以针对不同的情况抛出不同的异常,然后针对不同的异常进行不同的处理,这种情况下,内置的异常类型局限性太大,我们可以自定义异常类型。

一般情况下,我们可以通过继承 Error 创建自己的异常类型。

举个栗子:

// 定义用户名验证异常类
class UsernameValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "UsernameValidationError";
  }
}

// 定义密码验证异常类
class PasswordValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "PasswordValidationError";
  }
}

//-----------------------------------

// 校验用户名
function validateUsername(username) {
  if (typeof username !== "string" || username.trim().length < 3) {
    throw new UsernameValidationError("用户名必须是至少3个字符的字符串");  // 用户名格式不正确时,抛出异常
  }
}

// 校验密码
function validatePassword(password) {
  if (typeof password !== "string" || password.length < 6) {
    throw new PasswordValidationError("密码必须至少6位");  // 密码格式不正确时,抛出异常
  }
}

//-----------------------------------

// 使用示例
try {
  const username = "foooor.com"; // 太短
  const password = "123"; // 太短

  validateUsername(username);
  validatePassword(password);

  console.log("校验通过,允许登录");
} catch (err) {
  // 根据类型分别处理
  if (err instanceof UsernameValidationError) {
    console.log("用户名格式错误:" + err.message);
    alert('用户名格式不正确');
  } else if (err instanceof PasswordValidationError) {
    console.log("密码格式错误:" + err.message);
    alert('密码格式不正确');
  } else {
    console.log("未知错误:" + err.message);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
  • 在上面的代码中,首先定义了两个异常类型 UsernameValidationErrorPasswordValidationError 继承 Error 类。
  • 然后编写了两个校验方法,如果用户名和密码不正确就抛出响应的异常。
  • 在使用的时候,直接调用校验方法,并进行异常捕获,在捕获到异常的时候,进行异常类型判断,判断是用户名还是密码格式不正确,并给出提示。