JavaScript 错误处理

今天来复习一下 Javascript 的错误处理,顺便理一下自己开发中每天在书写的错误。

Overview

我们先来回忆一下 JS 常见的 Error Handling。

Callback

回调是 JS 在很长一段时间里捕获异步错误的唯一方式。看个例子,回忆一下回调地狱年代的代码:定义一个异步函数cbFunc,传入callback函数,在运行 1000ms 后捕获oops这个 Error。

function cbFunc(callback) {
  setTimeout(() => {
    callback(new Error('oops'));
  }, 1000);
  // happy path if any
  //callback(null, data)
}

回调极不直观,我刚入坑 JS 的时候,被它折磨过很久。上述代码中,cbFunc 的参数 callback 其实是个函数(没有 FP 经验的小朋友可能会转不过来)。callback 的第一个参数是 Error 类型的对象,第二个参数才是正确处理后的数据;而且在 JS 异步机制下,它的执行与 caller 不在一个 tick 里(不会阻塞 caller,只会在未来的某个时间执行)。

const cb = function callback(err, data) {
  if (err) {
    console.error(err.message);
  } else {
    // happy path and deal with data
  }
}

cbFunc(cb); // oops

有没发现?callback 的错误捕获其实很 naive,靠的是判断第一个参数是否为 null。

  • 失败:callback(new Error())
  • 成功:callback(null, data)

在没有类型定义的开发中,这种参数形式其实随意,各种前端报错;因此在刚开始的时间里,JS 只能作为 web 响应的辅助手段。

Promise

Promise 的正式出现要到 es6!很那想象,这么多年来,我们调用三方 JS 库时,出错处理主要靠自觉?!

function asyncFunc() {

  const executor = (resolve, reject) => {
     setTimeout(() => {
       reject(new Error('oops'))
     }, 1000);
     // happy path
     // resolve(data)
  }

  return new Promise(executor);
}

Promise 构造参数executor依旧难以理解,它本身是函数,两个参数(resolvereject)也还是函数。Promise 将执行时的错误抛给reject函数,而成功执行的结果则传给resolve函数。

虽然看起来有点复杂,但是我们至少可以在肉眼层面判断出asyncFunc会把执行成功的结果放在then里,失败的结果放在catch里。

asyncFunc()
 .then((data) => {
    // happy path
 })
 .catch((err) => {
    console.error(err.message)
 });

console.log('Hello');

不过 promise 依旧是异步方法,catch代码块的执行会晚于asyncFunc()的执行上下文。上述代码里,Hello打印将早于err.message。换句话说,错误处理只能发生在未来某个不确定的时间,asyncFunc()正下方的代码块依旧无法及时应对即将发生的错误。

async-await

async-await 其实就是 promise 和生成器的语法糖,跟随它们出现了一个新的语法 try-catch——JS 错误处理终于跟上了主流开发语言的节奏:

  • try 里是成功执行的代码块
  • catch 里是错误处理

而且 async-await 最大的改变是,我们终于可以在一个看似同步的过程中处理错误了。举个例子,如下代码中,console.log('Hello'); 一定晚于try-catch代码块执行。相比于 promise 这是巨大的进步。

try {
  await asyncFunc();
  // happy path
} catch (err) {
  console.error(err.message);
}

console.log('Hello');

当然,async-await 语法在某些场景下依旧会有许多让人困惑的地方。如下是两个很经典的例子:例 1 能捕获 asyncFunc 的错误,而例 2 不能。原因在于 async-await 本质是 promise 语法糖,return asyncFunc()是不会执行 promise 对象内的executor方法(见上文 asyncFunc 定义),真正的执行要等到await或是调用.then方法。例 1 执行了asyncFunc内部的异步调用,reject吃下的错误会在catch里抛出;而例 2 仅仅给调用者返回来一个待执行的 promise 对象,reject还没开始吃new Error('oops')

// Example 1
try {
  return await asyncFunc();
} catch (err) {
  // Any promise rejection while calling asyncFunc() will reach here, because of using `await`
}

// Example 2
try {
  return asyncFunc();
} catch (err) {
  // No promise rejection will reach here because the promise is returned to the caller instead of resolving it here.
}

Worst Practice

上文快速回顾了 JS 各个年代里捕获 Error 的方式。下面再谈谈开发中的出错经历。

没有处理未捕获的异常

开发中,即便你在代码外包了无数层 try-catch,你还是会遗漏掉一些特殊的错误。在 nodejs 中,这类遗漏的异常共两种,分别称作uncaughtExceptionunhandledRejection。Nodejs 程序最终会捕获这类异常,并在后台打印错误;但这个 log 并不能被我们自己的 logger 收集到。所以,生产环境应中应当主动监听到这类异常;甚至有些流派认为,发生这类异常就该直接杀死进程,并立即修复。方法很简单,如下:

process.on('uncaughtException', (err) => {
  logger.fatal('an uncaught exception detected', err);
  process.exit(-1);
});

process.on('unhandledRejection', (err) => {
  logger.fatal('an unhandled rejection detected', err)
  process.exit(-1);
});

隐藏错误

隐藏错误,指的是 caller 无从得知错误是否发生。如下代码,catch 块里直接返回了空数组,调用栈上下游将无从得知缘由——users 本身为空还是连接错误了?这类错误的表象是数据不一致,但是排查起来却困难重重。

// Bad example
function processUsers() {
    try {
        const body = await client.get('http://example.com/users');
        return body.users || [];
    } catch (err) {
       return []
    }
}

Best Practice 是:

  • 至少得打个 error log
  • 明确地为调用链下游传递错误信息:最简单的就是throw(err);此外,在 express 我们通常会调用next(err)

过多的 try-catch

上文提到不该隐藏错误,但是过多的 throw Error 会让代码到处都是 try-catch 块,及其难看;而且到处都在处理错误也是一件很麻烦的事。我曾经的一篇文章里提到过如何减少 try-catch 块,有兴趣的小伙伴可以再回看一下。核心思想就是建立一个统一的 error handler 模块——专门处理事件异常。

//error hanlder
if (err instanceof AuthenticationError) {
  return res.status(401).send('not authenticated');
}

if (err instanceof UnauthorizedError) {
  return res.status(403).send('forbidden');
}

// err omit...

// Generic error
return res.status(500).send('internal error occurred')

这就要求我们自定义错误类型。我想很多小朋友都没有实现过自定义的 Error 吧,这里做个演示。

class UserServiceError extends Error {
  constructor(...args) {
    super(...args);
    this.code = 400;
    this.name = 'UserServiceError';
    this.stack = `${this.message}\n${new Error().stack}`;
  }
}

实现如上,就是继承原生的 Error,然后自定义 code,name,stack 等信息。使用如下:根据特定请求抛出相应的异常。

app.use('user', async (req, res, next) => {
  try {
     const user = await getUserFromApi(req.headers.id);
     res.json(user);
  } catch (err) {
    next(new UserServiceError(err.message));
  }
})

未对日志分级

log 是生产线上排查错误的重要信息(有时候也是唯一信息来源)。很多小朋友只会用console.log这一种方式,事实上这样的日志意义不大:一旦出错我们很难在浩如烟海的日志中快速过滤出错误消息。合理的做法是:将 log 根据重要程度分成不同的级别,并在某些级别的日志出现时及时告警。以下五种分级是我们常用的一些日志分类方式:

  • debug:非重要信息,在开发环境里 debug 的一些消息
  • info: 比较重要的信息,用于追踪调用栈
  • warn: 警告,虽不至于出错,但是已经是需要排查的问题了
  • error:错误,需即刻注意的信息,用于排查 bug 发生的场景
  • fatal:致命错误,会导致服务停运的信息,需要立即修复

开发时正确地归类 log 能帮助运维更高效地定位错误;及时告警甚至能避免一些重大的事故。

小结

That's it. 这期我们回顾了 JS 错误捕获的几种方式,又列举了一些常见的误区。信息不多,就是归纳了一些我自己开发中就在书写的 Worst Practice。开发嘛,就是一个不断试错、纠正、总结的过程;记录一些小小的心得,希望与大家共同成长。

相关

《Express Middleware (续)》
文章同步发布于an-Onion的Github。码字不易,欢迎点赞。

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

推荐阅读更多精彩内容

  • Promise 对象 Promise 的含义 Promise 是异步编程的一种解决方案,比传统的解决方案——回调函...
    neromous阅读 8,698评论 1 56
  • 你不知道JS:异步 第三章:Promises 接上篇3-1 错误处理(Error Handling) 在异步编程中...
    purple_force阅读 1,384评论 0 2
  • 单线程 JavaScript是一门单线程的语言,被广泛应用于浏览器和页面DOM元素交互,自从Node.js出现后,...
    JunChow520阅读 796评论 0 3
  • 前言 编程语言很多的新概念都是为了更好的解决老问题而提出来的。这篇博客就是一步步分析异步编程解决方案的问题以及后续...
    李向_c52d阅读 1,064评论 0 2
  • 今天学习20180401 1.喜马拉雅时间管理进阶 【试听】最好用不容易用好的神器 【试听】随堂练习:月度日程表十...
    一日看人生阅读 173评论 0 0