异步错误的获取

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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • NodeJS 的错误处理让人痛苦,在很长的一段时间里,大量的错误被放任不管。但是要想建立一个健壮的 Node.js...
    宫若石阅读 725评论 0 3
  •   由于 JavaScript 本身是动态语言,而且多年来一直没有固定的开发工具,因此人们普遍认为它是一种最难于调...
    霜天晓阅读 739评论 0 1
  • 从小就生活在一个小镇,这里依山傍水,没有夏日里将人灼伤的阳光,也没有冬日里纷飞三天三夜的雪,气候宜人。然而...
    简深js阅读 247评论 0 1
  • 2017年9月11日晚八点四十七,希望自己可以睡的很早,白天有要忙碌的事,晚上没有要想念的人。 1709...
    不灵哎呀阅读 364评论 0 0
  • 一切技巧都是为具体事实服务的,一定要真情实感! 在晚上吃饭的时候,加了超级行动力的明月老师,被说服了,加入了超级行...
    林亦凡阅读 84评论 0 0