异常处理
1、捕获异常
在编写代码时,经常在控制台看到TypeError、RangeError、ReferenceError等开头的红色错误信息,这些都是程序发出的警告,告知开发者出现了什么问题,并附加与代码有关的信息,例如出错的位置。这时可以在代码出错的地方使用try...catch语句块对异常进行捕获和处理。
try{}语句块用于监控内部的代码,catch{}语句块用于捕获到异常后编写异常处理语句。假设如果一个值应该为对象的变量,而实际上其值为undefined,则调用它的方法就会抛出TypeError异常,代码如下:
js
let obj=undefined;
obj.method(); //Uncaught TypeError:Cannot read property 'method'of undefined
//未捕获的类型异常:不能访问undefined的method属性
console.log("这行代码不会被执行");
在异常抛出后,它后边的代码便不会被执行了,如果obj.method()后边还有其他代码,则它们永远不会被执行,使用try...catch把异常捕获之后就能解决这个问题了,代码如下:
js
try{
let obj=undefined;
obj.method();
}catch(e){
console.error(e.name);
console.error("不能访问undefined中的方法");
}
console.log("此行能正常执行");
js
TypeError
不能访问undefined中的方法
此行能正常执行
catch语句块中的小括号接收一个可选的参数(如果不接收则不写小括号),是try中代码抛出来的Error对象,稍后再介绍有关Error的内容,这里访问了它的name属性,打印出了具体的Error对象名字,后面打印了自定义的异常信息,提示不能访问undefined中的方法。这样异常就被捕获住了,并且后边的代码还会正常执行。在catch语句块打印日志时,可以使用console.error()以让控制台的显示格式为错误信息。
如果只想捕获异常但不处理,则也可以省略catch语句块的参数和其中的内容,代码如下:
js
try{
let obj=undefined;
obj.method();
}catch{}
2、throw抛出异常
有时候可能明确知道需要在什么地方抛出异常,而不是仅靠JavaScript运行时自动判断,例如判断函数参数是否符合规则,如果不符合就抛出异常,这时可以通过throw关键字自行抛出异常。如果后续使用catch语句块捕获了该异常,则throw后边的表达式会传递给catch的参数,如果没有捕获异常,则会直接将异常打印到控制台。另外,throw后边的代码不会被执行。throw关键字用法的代码如下:
js
function setName(name){
if(!name)throw"name不能为空";
console.log("这行代码不会被执行");
}
//未捕获异常
setName(); //name不能为空
捕获异常:
js
try{
setName();
}catch(e){
console.error(e);//name不能为空
}
它会打印出"name不能为空",e参数的值就是throw后边抛出的字符串。
throw关键字后边可以是任何类型的值或表达式,例如下方的throw语句都是合法的:
js
throw 10; //抛出数字
throw false;//抛出布尔值
throw[]; //抛出数组
throw{}; //抛出对象
throw语句几乎可以用在任何地方。可以在全局代码中、普通函数中、构造函数中、对象或类的方法中,但是不能用在需要表达式的地方,例如数组元素、函数参数、对象属性值等当中。
如果throw语句嵌套的层次比较深,则可以在任何一层进行异常处理,例如当编写后端Web应用程序时,一般会在处理请求和响应的控制器中统一处理用户输入错误、数据库错误和业务逻辑错误等,这样下层的代码可以直接抛出异常,然后在最上层统一进行处理,代码如下:
js
function mapArr(arr){
arr.map((v)=>{
if(v===2){
throw"error";
}
console.log(v);
});
}
try{
mapArr([1,2,3]); //1 error
}catch(error){
console.log(error);
}
mapArr使用数组的map()方法遍历参数数组,在map()的回调函数中,判断如果遍历到的值为2就抛出异常,其他值则直接打印出来。代码的输出结果为1error,这是因为throw语句后边的代码不会被执行,所以在遍历数组元素2时,map()回调函数就停止执行了。
这时throw语句在map()的回调函数中,后边可以在调用mapArr()的时候再去捕获异常,就像上方代码一般。另外也可以在mapArr()函数中捕获异常,代码如下:
js
function mapArr(arr){
try{
arr.map((v)=>{
if(v===2){
throw"error";
}
console.log(v);
});
}catch(error){
console.log(error);
}
}
具体在何时处理异常,就要看具体的业务需求和对代码的影响。
如果在一段代码中使用了可能抛出异常的代码,并且想做一些处理操作之后把相同的异常再次抛出,则可以直接在catch{}语句块中使用throw语句把catch的参数抛出。假设有一系列处理用户请求的函数,在收到查询某个特定用户信息的请求之后,由控制器交给业务逻辑层处理,再由业务逻辑层去查询数据库,当数据库出错时,在业务逻辑层做一些处理操作(例如记录日志),然后直接把异常抛给控制器处理,这时就可以使用再抛出(Rethrow)来把异常原样交给上层去处理,代码如下:
js
function queryDb(id){
throw"未在数据库中找到该条记录";
}
function getUserByIdService(id){
try{
queryDb(id);
}catch(error){
//一些其他操作
throw error;
}
}
function getUserController(id){
try{
getUserByIdService(id);
}catch(error){
console.log(error);
}
}
getUserController(1);
在getUserByIdService()函数中,当在catch里处理完其他操作之后,使用了throw把error参数抛出,之后在getUserController()中处理了这个异常。
throw后边也可以抛出更具有实际意义的Error对象,11.3节来看一下它的用法。
3、Error对象
Error是JavaScript内置的对象,用于表示异常信息,它有name和message两个属性,分别为异常的名字和消息,它还有一个toString()方法,用于把异常信息转换为字符串。这些属性和方法是浏览器和Node.js都支持的,不过对于不同的浏览器,Error还会有更多的属性,例如lineNumber异常出现的行号、columnNumber异常出现的列号和stack异常的堆栈信息。
不过,Error对象是基础对象,一般需要通过继承的方式来定义更明确的异常对象。JavaScript根据Error对象扩展出了如下几种内置的异常对象。
- TypeError:表示变量或参数不是正确的类型,例如访问undefined中的方法。
- RangeError:表示数字类型的变量或参数超出了指定范围或无效。例如new Array(NaN)。
- ReferenceError:在引用不存在的变量时抛出,例如console.log(a)。
- SyntaxError:在使用错误的语法时抛出。例如let 32=5(使用了数字作为变量名)。
这些异常的name属性值就是各自的构造函数的名字,如TypeError、RangeError。
在使用throw语句时,可以在它后边的表达式中直接创建Error对象,或创建上述内置的其他异常对象,这样可以让异常包含更丰富的信息,以便于开发者根据提示处理异常。要创建这些异常对象,可以直接使用它们的构造函数,并传递一个message参数表示错误消息,代码如下:
js
function division(a,b){
if(b===0)throw new Error("除数不能为0");
return a/b;
}
division(5,0); //Error:除数不能为0
//at division(<anonymous>:2:21)
//at<anonymous>:1:1
示例中的division()函数用于除法计算,如果除数为0就抛出异常。
当后边给参数b传递值0时,该异常就会抛出,控制台会打印出"Error:除数不能为0"的异常消息和堆栈信息。异常消息是通过调用Error中的toString()转换成的字符串,其中冒号前边为Error对象中name属性的值,即异常的名字,后边是message属性的值。上述throw语句也可以抛出一个更明确的RangeError:throw newRangeError("除数不能为0"),这样异常对象的name属性就会变为RangeError,打印出来的信息会变为"RangeError:除数不能为0"。
4、自定义异常
大部分情况下JavaScript内置的异常对象不能满足业务的要求,一般的程序中都应该定义自己的异常对象来生成更具有业务意义上的异常对象,例如RESTful API请求异常、数据验证异常等。通过继承Error对象,然后修改其中默认的错误消息和处理方式,并添加自定义的异常信息和业务逻辑,就可以快速创建自定义的异常对象。
假设程序在接收到用户输入之后需要判断数据是否符合验证规则,如果不符合则抛出异常,那么这个异常可以定义为 ValidationError,它除了包括既有的name和message属性之外,还包括用户原始输入信息,要创建它,可以通过原型方式或class方式,由于class方式比较直观,所以本示例使用class实现继承,代码如下:
js
class ValidationError extends Error{
constructor(message,input){
super(message+",用户输入:"+input);
this.name=ValidationError.name;
this.input= input;
}
}
function validatePassword(pwd){
if(!pwd||pwd.length<8)
throw new ValidationError("密码不能小于8位",pwd);
return true;
}
try{
validatePassword("123456");
}catch(e){
console.log(e instanceof ValidationError);
console.log(e.name);
console.log(e.message);
console.log(e.input);
}
ValidationError继承了Error对象,在构造函数中首先调用父类Error的构造函数,使用super(message+",用户输入:"+input),初始化了message属性,并在message属性值的基础上加上了用户输入的值。后边给ValidationError类新添加了一个input属性,用于保存用户原始输入信息,name属性则设置为当前类的name属性值,即ValidationError。
validatePassword()函数用于简单地判断密码是否大于或等于8位,如果不是则抛出ValidationError,并分别设置异常消息和用户原始输入。在调用这种方法时,使用try...catch捕获了这个异常,并打印了异常的信息,判断它是不是ValidationError类的实例、异常的名字、消息和用户输入。上述代码的输出结果如下:
js
true
ValidationError
密码不能小于8位,用户输入:123456
123456
在日常开发中应尽量使用自定义的异常,这样能够使异常信息更具体,更符合实际的业务逻辑。
5、finally
在try...catch语句块中,还可以添加可选的finally语句块,一般用于在抛出异常后执行一些收尾和清理操作,例如关闭数据库或文件访问句柄。finally语句块的代码里边的语句必定会被执行,无论try或catch是否执行。如果有catch语句块,则finally语句块会在catch语句块之后被执行,如果没有catch语句块则会在try语句块之后被执行。finally语句用法的代码如下:
js
try{
console.log("获取数据库连接对象");
throw"出现错误"
}catch{
console.log("不能获取连接");
}finally{
console.log("关闭数据库对象")
}
上述代码会输出的结果如下:
js
获取数据库连接对象
不能获取连接
关闭数据库对象
可以看到try、catch和finally的语句块都按顺序执行了,这时如果把try中的throw注释掉,则catch中的"不能获取连接"就不会打印,但是finally中的"关闭数据库对象"仍然会打印。
不过需要注意的是,如果try中有return语句,则finally语句仍然会被执行,且在return语句后被执行,例如把上方代码稍做改动,放到函数中,并添加一个conn变量和return语句,来测试一下finally和return的执行顺序,代码如下:
js
function getConnection(){
let conn=null;
try{
console.log("获取数据库连接对象");
conn="连接对象";
return conn;
}catch{
console.log("不能获取连接");
}finally{
console.log("关闭数据库对象");
conn="连接已关闭";
}
}
let conn=getConnection();
console.log(conn); //"连接对象"
可以看到conn先于return被执行出来了,console.log(conn)打印出了finally对conn进行修改前的值,为了进一步验证可以在finally再加上一个return语句,代码如下:
js
finally{
console.log("关闭数据库对象");
conn="连接已关闭";
return conn;
}
这次打印出来了"连接已关闭",因为finally语句块最后被执行,所以它里边的return覆盖掉了try中的return语句。当然这里只是为了演示它们的顺序,在实际开发中不应该在同一函数中有两个return语句。
在捕获异常时,catch和finally语句块至少需要存在一个,要么是try...catch,要么是try...finally,要么是try...catch...finally,如果只有一个try{}语句,则程序会抛出语法错误,提示try后边缺少catch或finally语句,代码如下:
js
try{
throw"异常"
}
//语法错误:try后边缺少catch或finally语句
6、捕获多个异常
有些程序代码可能会同时抛出多个异常,在常见的后端开发中,数据库的访问、业务逻辑的处理、用户请求的处理等都有可能抛出异常,然后在控制器(最上层处理用户请求的地方)中统一进行异常处理,这时需要根据不同的异常类型来响应不同的HTTP状态码。
不过,JavaScript不像Java中可以用多个catch语句块捕获多种不同类型的异常,它只支持一个catch语句块,在里边可以通过if/else语句进行异常判断。例如下方示例在一个函数中返回了不同类型的异常,并且在catch语句块中同时处理,代码如下:
js
function division(a,b){
if(typeof a!=="number")throw new TypeError("a必须为数字");
if(typeof b!=="number")throw new TypeError("b必须为数字");
if(b===0)throw new RangeError("除数不能为0");
return a/b;
}
try{
division(1,"a");
//division(1,0);
}catch(e){
if(e instanceof TypeError){
console.log("类型不正确");
}else if(e instanceof RangeError){
console.log("取值不正确");
}else{
console.log(e);
}
}
上述代码会输出"类型不正确",如果把division(1,"a")注释掉,并取消division(1,0)的注释,则会打印出"取值不正确"。