JS 常见错误
当 JavaScript 引擎执行 JavaScript 代码时, 会发生各种错误, 常见的错误类型有
SyntaxError.
SyntaxError 是解析代码时发生的语法错误
- var 1a; // 变量名错误
- console.log 'hello'); // 缺少括号
- ReferenceError
ReferenceError 是引用一个不存在的变量时发生的错误.
比如在函数中调用一个变量, 但是这个变量不存在的时候
TypeError
TypeError 是变量或参数不是预期类型时发生的错误. 比如, 对字符串, 布尔值, 数值等原始类型的值使用 new 命令, 就会抛出这种错误, 因为 new 命令的参数应该是一个构造函数.
var obj = {}; obj.handle() //obj.handle is not a function
还有一些这里就不一一介绍了.
try catch
使用
总之, 我们在写代码的时候, 程序可能遇到无法预测的异常情况而报错, 从而阻塞代码执行, 例如, 网络连接中断, 读取不存在的文件等.
上面的错误如果从产生的阶段上来划分的话, 可以分成编译阶段和执行阶段的错误.
对于执行阶段的错误, 我们通常使用 try catch 来捕获这种错误并处理它.
当代码块被 try { ... } 包裹的时候, 一旦发生错误, 就不再继续执行后续代码, 转而跳到 catch 块. catch (e) { ... } 包裹的代码就是错误处理代码, 变量 e 表示捕获到的错误.
- try{
- console.log('step1');
- var s = null;
- console.log(s.length) // 产生错误
- console.log('step2');
- } catch(e){
- console.log('error');
- }
- //output
- step1
- error
抛出错误
程序也可以主动抛出一个错误, 让执行流程直接跳转到 catch 块. 抛出错误使用 throw 语句.
实际上, JavaScript 允许抛出任意对象, 包括数字, 字符串. 但是, 最好还是抛出一个 Error 对象.
catch 捕获
JavaScript 有一个标准的 Error 对象表示错误, 还有从 Error 派生的 TypeError,ReferenceError 等错误对象.
我们在处理错误时, 可以通过 catch(e) 捕获的变量 e 访问错误对象. 我们抛出的错误其实是继承在这个 Error 对象. 我们在 throw 的 new Error 传的参数就是 error 实例的 message 属性.
错误传播
如果在一个函数内部发生了错误, 它自身没有捕获, 错误就会被抛到外层调用函数, 如果外层函数也没有捕获, 该错误会一直沿着函数调用链向上抛出, 直到被 JavaScript 引擎捕获, 代码终止执行.
- try {
- try{
- throw new Error('这是抛出的错误');
- } catch(e) {
- console.log('内层捕获的错误');
- }
- } catch(e) {
- console.log('外层捕获的错误');
- }
- //output
- 内层捕获的错误
- finally
最后, 无论有没有错误, finally 一定会被执行.
异步错误
如果 try 模块里面是通过异步操作抛出的异常, 异常就不能正常捕获到. 比如:
- try{
- setTimeout(()=>{
- throw new Error('fail');
- },1000);
- } catch (e){
- console.log(e);
- }
这是因为异步调用是立即返回的, 因此当发生异常的时候, 已经脱离了 try..catch.. 的上下文了, 所以异常无法被捕获.
异步错误的解决方案
异步代码内部直接捕获错误
如果是异步的异常, 那就在异步代码或者回调函数里捕获异常.
- setTimeout(()=>{
- try{
- throw new Error('fail');
- }catch (e){
- console.log(e);
- }
- },1000);
使用 Promise
- var p1 = function(){
- return new Promise(function(resolve,reject){
- throw new Error('p1_同步_err'); // 代码 1
- setTimeout(()=>{
- console.log('p1 执行')
- resolve(true)
- // throw new Error('p1_异步_err'); // 代码 2
- // reject('p1_rej') // 代码 3
- },1000)
- })
- }
- var p2 = function(){
- return new Promise(function(resolve,reject){
- // throw new Error('p2_同步_err'); // 代码 4
- setTimeout(()=>{
- console.log('p2 执行')
- // throw new Error('p2_异步_err'); // 代码 5
- // reject('p2_rej') // 代码 6
- },1000)
- })
- }
- p1().then(p2).catch(function(err){
- console.log('catch 里的错误')
- console.log(err)
- })
- //output1
- catch 里的错误
- a.html:44 Error: p1_同步_err
- at a.html:23
- at new Promise (<anonymous>)
- at p1 (a.html:22)
- at a.html:42
- //output2
- p1 执行
- a.html:27 Uncaught Error: p1_异步_err
- at setTimeout (a.html:27)
- //output3
- p1 执行
- a.html:43 catch 里的错误
- a.html:44 p1_rej
做了一组试验. 六种情况. 比如
情况 1, 代码 1 执行, 其它的代码 2-6 都注释掉.
情况 2, 代码 2 执行, 其它的代码 1,3-6 都注释掉.
情况 3, 代码 3 执行, 其它的代码 1,2,4-6 都注释掉.
结论: 对于 promise 而言
如果是同步执行, throw 出去的错误可以捕获, 而异步执行的不可以
如果是异步执行, throw 出去的错误不可以被捕获, 只能通过 reject 来传递.
使用 async 和 await
- async function doSomething() {
- return new Promise((resolve, reject) => {
- setTimeout(() => {
- throw new Error("fail");
- }, 1000);
- });
- }
- async function main() {
- try {
- await doSomething();
- } catch (e) {
- console.log(e.message);
- }
- }
- main();
效果类似. 因为 async await 实现方式本身也是基于 generator+promise
浏览器环境
在浏览器中, 很多时候出现 Error, 整个页面就挂掉了. 在 window 对象下有个 onerror 属性. 我们可以给 window.onerror 属性加上一个回调来实现对前端代码的监控. onerror 函数会在页面发生 js 错误时被调用.
window.onerror 可以拿到出错的信息以及文件名, 行号, 列号, 如果在 onerror 函数中 return true 可以让浏览器不输出错误信息到控制台.
否则会在控制台中显示错误消息.
nodeJs 环境
需要说明的是, 在 node 中大多数的异步方法都接受一个 callback 函数, 该函数会接受一个 Error 对象传入作为第一个参数. 如果第一个参数不是 null 而是一个 Error 实例, 则说明发生了错误, 应该进行处理.
- const fs = require('fs');
- fs.readFile('一个不存在的文件', (err, data) => {
- if (err) {
- console.error('读取文件出错!', err);
- return;
- }
- // 否则处理数据
- });
- uncaughtException
uncaughtException 其实是 NodeJS 进程的一个事件. 如果进程里产生了一个异常而没有被任何 Try Catch 捕获会触发这个事件.
NodeJS 对于未捕获异常的默认处理是: 沿着代码调用路径反向传递回事件循环 - 触发 uncaughtException 事件 - 如果 uncaughtException 没有被监听, 那么 - 打印异常的堆栈信息 - 触发进程的 exit 事件. Node.js 原生提供 uncaughtException 事件挂到 process 对象上, 用于捕获所有未处理的异常.
- process.on('uncaughtException', function (err) {
- console.log('uncaughtException error');
- });
- try {
- setTimeout(function(){
- throw new Error('fail');
- },1000);
- } catch (e) {
- console.log("catch error")
- }
- //output
- uncaughtException error
- uncaughtException
需要注意, 如果打算使用'uncaughtException' 事件作为异常处理的最后补救机制, 这是非常粗糙的设计方式. 未处理异常本身就意味着应用已经处于了未定义的状态. 如果基于这种状态, 尝试恢复应用正常进行, 可能会造成未知或不可预测的问题.
而且 uncaughtException 错误会导致当前的所有的用户连接都被中断, 甚至不能返回一个正常的 HTTP 错误码, 这是由于 uncaughtException 丢失了当前环境的堆栈, 导致 Node 不能正常进行内存回收, 比如下面的例子
- app.get('/', function (req, res) {
- setTimeout(function () {
- throw new Error('async error');
- res.send(200);
- }, 1000);
- });
- process.on('uncaughtException', function (err) {
- res.send(500); // 做不到, 拿不到当前请求的 res 对象
- });
- domain
如果可以通过某种方式来捕获回调函数中的异常, 那么就不会有 uncaughtException 错误导致的崩溃. 为了解决这个问题, Node 0.8 之后的版本新增了 domain 模块, 它可以用来捕获回调函数中抛出的异常.
domain 模块, 把处理多个不同的 IO 的操作作为一个组. 注册事件和回调到 domain, 当发生一个错误事件或抛出一个错误时, domain 对象会被通知, 不会丢失上下文环境, 也不导致程序错误立即退出.
domain 主要的 API 有 domain.run 和 error 事件. 通过 domain.run 执行的函数中引发的异常都可以通过 domain 的 error 事件捕获, 例如:
- var app = express();
- var server = require('http').createServer(app);
- var domain = require('domain');
- app.use(function (req, res, next) {
- var reqDomain = domain.create();
- reqDomain.on('error', function (err) { // 下面抛出的异常在这里被捕获
- res.send(500, err.stack); // 成功给用户返回了 500
- });
- reqDomain.run(next);
- });
- app.get('/', function () {
- setTimeout(function () {
- throw new Error('async exception'); // 抛出一个异步异常
- }, 1000);
- });
上面的代码将 domain 作为一个中间件来使用, 保证之后 express 所有的中间件都在 domain.run 函数内部执行. 这些中间件内的异常都可以通过 error 事件来捕获. 我们可以正常的给用户返回 500 错误.
所以, 我们可以结合两种异常捕获机制, 用 domain 来捕获大部分的异常. 对于剩下的异常, 通过 uncaughtException 事件来避免服务器直接 crash.
来源: http://www.jianshu.com/p/e290c5f7274e