边学边译JS工作机制---20.JS的异常处理以及在异步中处理异常的办法

265 阅读7分钟

本系列其他译文请看[JS工作机制 - 小白1991的专栏 - 掘金 (juejin.cn)] (juejin.cn/column/6988…

本文阅读指数:5
异常处理在前端工程中是非常常见,也非常重要的。但是重视的人并不多。建议充分阅读。

概述

异常检测是保证程序能够可靠运行的一个技术手段。

异常检测的一个途径是异常检查。异常检查维持正常的程序流程,同时进行异常的检查,它使用特殊的返回值,辅助的全局变量或者浮点状态值来上报异常。

异常在程序执行的过程中发生,并且会中断正常的流程。这样的中断会触发预定义的异常处理函数。

注意,硬件和软件都可能发生异常。

JS中的异常

一个JS应用可以运行在各种操作系统,浏览器或者硬件设备中。无论你写多少测试,面对这么复杂的环境,总是会有异常的。 从终端用户的角度看,JS都是默默处理异常的。但是其背后的机制却有点复杂。 只要一部分代码出错,JS就会抛出一个异常。JS引擎就不会继续执行代码了,而是要检查一下是否有异常处理的句柄函数。

如果没有异常句柄,引擎就会return,然后抛出一个异常。然后调用栈中的每一个函数都会重复这个步骤,直到找到处理异常的句柄。如果到最后都没有找到句柄,栈中也没有函数了,那么event loop就会把回调队里中的下一个函数加入到栈中。

异常发生时,会生成一个Error对象,并抛出异常。

Error对象的类型

JS内置了9中异常对象,他们是异常处理的根本:

  • Error - 表示一般通用的异常,经常用来实现用户自定义的异常。
  • EvalError- 没有正确使用eval()函数时发生的
  • RangeError - 访问数字变量或者参数时,超出了它的可能范围时发生
  • ReferenceError - 访问一个不存在的变量时发生
  • SyntaxError - 没有遵循JS语法规则时发生。对于静态语言,这个错误是在编译时触发。对于JS则是在执行时触发。
  • TypeError — 当一个值跟预期类型不匹配时发生。调用一个不存在的对象方法,也会引起这个异常
  • URIError — 调用encodeURI() 和 decodeURI() 时遇到了不合法的URL
  • AggregateError — 多个异常需要被合并到一次上报中时发生,比如Promise.any()
  • InternalError — JS引擎内部抛出的异常。比如“递归太多”,这个API目前还不是标准化的。

通过继承内置异常,你还可以自定义异常类型。

抛出异常

JS允许开发者调用throw来触发异常。

if (denominator === 0) {
    throw new RangeError("Attempted division by zero");
}

每一个内置的异常对象,具有一个可选的'message'参数,这样可以异常描述可读性更好。

你可以抛出任何类型的异常,比如数字,字符串,数组等等

throw true;
throw 113;
throw ‘error message’;
throw null;
throw undefined;
throw {x: 1};
throw new SyntaxError(‘hard to debug’);

这些都是有效的JS 声明。

使用内置的异常类型比其他对象摇号,因为浏览器会特殊照顾他们,比如引起异常的文件名,行数,调用栈跟踪等。一些浏览器,比如firefox,会为所有类型的异常对象收集这些属性

处理异常

现在看看如何保证异常不会摧毁我们的应用吧。

“try” 语句

跟其他编程语言类似,JS 具有trycatchfinally声明,让我们得以控制异常流。

比如:

try {
    // a function that potentially throws an error
    someFunction();
} catch (err) {
    // this code handles exceptions
    console.log(e.message);
} finally {
    // this code will always be executed
    console.log(finally’);
}

try语句强制性的包裹住可能抛出异常的代码块。

“catch” 语句

“catch” 语句紧随其后,它包住了异常处理的代码块。“catch” 语句让异常不在扩散,允许程序继续执行。 异常本身作为一个参数被传递到catch语句。

一些代码块可以抛出不同类型的异常,你的应用可以支持多种异常。 instanceof操作可以用来区分不同类型的异常

try {
  If (typeof x !== ‘number’) {
       throw new TypeError(‘x is not a number’);
  } else if (x <= 0) {
       throw new RangeError(‘x should be greater than 0’);
  } else {
       // Do something useful
  }
} catch (err) {
    if (err instanceof TypeError) {
      // Handle TypeError exceptions
    } else if (err instanceof RangeError) {
      // Handle RangeError exceptions
  } else {
      // Handle all other types of exceptions
  }
}

这个例子可以重抛一个捕获的异常。比如你虽然捕获了异常,但是它跟你的上下文没有关系,那就可以再次抛出去。、

“finally” 语句

finally代码块在try 和 catch之后执行,无视任何异常(话句话说,只要发生了异常,那么finally就一定会执行)。finally语句可以用来执行一些清理工作,比如关闭WebSocket连接等。

即使异常没有被捕获,finally块也会执行。在这种场景下,finally块执行之后,引擎会继续按顺序检查调用栈中的函数,直到找到正确的异常句柄或者直到应用被关闭。

同样,即使try 或者catch已经执行了了return ,finally也还是会执行。

看一个例子:

function foo1() {
    try {
        return true;      
    } finally {
        return false;
    }
} 

执行 foo1()函数,得到返回值false ,即使try已经有一个return 声明了。

下面的例子是同样的结果:

function foo2() {
    try {
        throw new Error();      
    } catch {
        return true;
    } finally {
        return false;
    }
} 

执行 foo1()函数,得到返回值false 

处理异步代码中的异常

之前讨论过JS中异步编程的机制,这里我们看看如果处理“callback functions”, “promises”, 和 “async/await”中的异常。

async/await

定义一个标准的函数,抛出一个异常

 async function foo() {
     throw new Error();
 }

当异常在async 函数中抛出时,返回的是一个‘rejected’的promise,并伴随了抛出的异常

return Promise.Reject(new Error())

看看当调用foo()时发生了什么

try {
    foo();
} catch(err) {
    // This block won’t be reached.
} finally {
    // This block will be reached before the Promise is rejected.
}

由于foo()是异步,它分发了一个 Promise。代码不会等待async 函数结束,所以此时其实没有真正的捕捉到异常。finally块会执行,然后返回一个Promise 并且rejected。 我们没有任何代码来处理这个被rejected的Promise。 在调用foo()时可以添加一个await关键字,并且用async函数包含这段代码,就可以处理这个promise了。

async function run() {
    try {
        await foo();
    } catch(err) {
        // This block will be reached now.
    } finally {
        // This block will be reached at the end.
    }
}
 run();

Promises

顶一个函数,在Promise外面扔出一个异常

function foo(x) {
    if (typeof x !== 'number') {
        throw new TypeError('x is not a number');
    }

    return new Promise((resolve, reject) => {
        resolve(x);
    });
}

现在给 foo 传递一个 string而不是number

foo(‘test’)
    .then(x => console.log(x))
    .catch(err => console.log(err));

这会引起Uncaught TypeError: x is not a number ,因为promise的catch还不能处理异常--它是在Promise之外抛出的

使用标准的try 和 catch 语句可以捕获这个异常

try {
    foo(‘test’)
    .then(x => console.log(x))
    .catch(err => console.log(err));
} catch(err) {
    // Now the error is handed
}

如果修改foo ,在Promise内部抛出异常

function foo(x) {
    return new Promise((resolve, reject) => {
       if (typeof x !== 'number') {
            throw new TypeError('x is not a number');
       }
        resolve(x);
    });
}

现在catch声明就会处理这个异常

try {
    foo(‘test’)
    .then(x => console.log(x))
    .catch(err => console.log(err)); // The error is handled here.
} catch(err) {
    // This block is not reached since the thrown error is inside of a Promise.
}

注意,在Promise 中抛出异常和使用reject回调是一样的。所以可以这样定义foo

function foo(x) {
    return new Promise((resolve, reject) => {
       if (typeof x !== 'number') {
            reject('x is not a number');
       }
        resolve(x);
    });
}

如果没有catch方法来处理promise内部的异常,回调队列中的下一个函数就会被添加到调用栈上。

Callback Functions

使用异常优先的回调策略有两个主要原则:

  1. 回调的第一个参数是error对象,如果发生了异常,它的第一个参数会被设置为err然后返回。如果没有异常,err为null
  2. 回调的第二个参数是响应过来的数据
function asyncFoo(x, callback) {
    // Some async code...
}

asyncFoo(‘testParam’, (err, result) => {
    If (err) {
        // Handle error.
    }
    // Do some other work.
});

如果有一个 err对象,最好不要触碰或者依赖result 参数。

未处理的异常怎么办

如果使用了第三方的库,你就没有权限去处理异常了。当你想处理一些没有句柄的异常时,可以看看下面的例子

浏览器

浏览器中的window.onerror事件可以处理这种情况: 例子:

window.onerror = (msg, url, line, column, err) => {
     // ... handle error …
     return false;
};

它的参数是这样的:

  • msg — 异常关联的信息,比如Uncaught ReferenceError: foo is not defined
  • url — 跟这个异常有关的脚本或者文档的URL
  • lineNo — 代码行数(如果有)
  • columnNo — 代码列数(如果有)
  • err — 异常对象(如果有).

如果一个函数返回true,会阻止默认事件句柄的触发。

每次只能给window.onerror 赋值一个事件句柄。 这意味如果你想赋值,那么就要覆盖之前的被第三方库已经写好的句柄。这可能会引起大问题,尤其是像一些异常跟踪的工具,它们可能完全停摆。 使用下面的小技巧,可以解决这个问题

var oldOnErrorHandler = window.onerror;
window.onerror = (msg, url, line, column, err) => {
    If (oldOnErrorHandler) {
        // Call any previously assigned handler.   
	oldOnErrorHandler.apply(this, arguments);
    }

    // The rest of your code
}

这个代码首先检查了是否之前已经定义好了window.onerror,然后在处理前简单的调用一下。使用这种办法,就可以随心所欲的在window.onerror上加句柄了

这种方式在各个浏览器中都能实现。

另外一个不需要替代句柄的方式,是给window对象加事件监听

window.addEventListener('error', e => { 
    // Get the error properties from the error event object 
    const { message, filename, lineno, colno, error } = e; 
});

这种方式更好,支持更广泛

Node.js

EventEmmiter模块中的process对象,提供两个事件来处理异常:

  1. uncaughtException -- 当一个未捕获的异常冒泡到了[event loop]时会发生。Node.js默认会把这异常的栈跟踪打印到stderr,然后退出并返回code 1。可以为这这个事件添加一个句柄。使用这个事件的恰当的方式是在线程关闭之前,执行异步的资源清理(比如文件描述符,句柄等等)。这之后再执行正常操作是不安全的。
  2. unhandledRejection —当Promise被rejected,并且没有异常处理时触发。在探查和跟踪被rejected并且没有异常句柄的promise时,unhandledRejection很有用。
process
    .on('unhandledRejection', (reason, promise) => {
         // Handle failed Promise
    })
    .on('uncaughtException', err => {
        // Handle failed Error   
        process.exit(1);
     });

在代码中正确的处理异常是非常重要的,只有理解未处理异常,你才能够正确的处理它们。

你可以自己做,但是可能会有点麻烦,你需要考虑各种浏览器的不同场景。你也可以使用一些第三方的的工具来实现。不管怎么做,你都需要尽可能多的理解异常和异常触发的上下文,这样才能轻松的复现异常。