前端魔法堂——异常不仅仅是try/catch-阿里云开发者社区

开发者社区> 肥仔john> 正文

前端魔法堂——异常不仅仅是try/catch

简介: 编程时我们往往拿到的是业务流程正确的业务说明文档或规范,但实际开发中却布满荆棘和例外情况,而这些例外中包含业务用例的例外,也包含技术上的例外。对于业务用例的例外我们别无它法,必须要求实施人员与用户共同提供合理的解决方案;而技术上的例外,则必须由我们码农们手刃之,而这也是我想记录的内容。
+关注继续查看

前言

 编程时我们往往拿到的是业务流程正确的业务说明文档或规范,但实际开发中却布满荆棘和例外情况,而这些例外中包含业务用例的例外,也包含技术上的例外。对于业务用例的例外我们别无它法,必须要求实施人员与用户共同提供合理的解决方案;而技术上的例外,则必须由我们码农们手刃之,而这也是我想记录的内容。
 我打算分成《前端魔法堂——异常不仅仅是try/catch》和《前端魔法堂——调用栈,异常实例中的宝藏》两篇分别叙述内置/自定义异常类,捕获运行时异常/语法异常/网络请求异常/PromiseRejection事件,什么是调用栈和如何获取调用栈的相关信息。
 是不是未出发就已经很期待呢?好吧,大家捉紧扶手,老司机要开车了^_^

概要

 本篇将叙述如下内容:

  1. 异常还是错误?它会如何影响我们的代码?
  2. 内置异常类型有哪些?
  3. 动手写自己的异常类型吧!
  4. 捕获“同步代码”中的"运行时异常",用try/catch就够了。
  5. "万能"异常捕获者window.onerror,真的万能吗?
  6. Promise.reject也抛异常,怎么办?
  7. 404等网络请求异常真心要后之后觉吗?

一.异常还是错误?它会如何影响我们的代码?

 在学习Java时我们会被告知异常(Exception)和错误(Error)是不一样的,异常是不会导致进程终止从而可以被修复(try/catch),但错误将会导致进程终止因此不能被修复。当对于JavaScript而言,我们要面对的仅仅有异常(虽然异常类名为Error或含Error字样),异常的出现不会导致JavaScript引擎崩溃,最多就是让当前执行的任务终止而已。
 上面说到异常的出现最多就是让当前执行的任务终止,到底是什么意思呢?这里就涉及到Event Loop的原理了,下面我尝试用代码大致说明吧。

<script>
  // 1.当前代码块将作为一个任务压入任务队列中,JavaScript线程会不断地从任务队列中提取任务执行;
  // 2.当任务执行过程中报异常,且异常没有捕获处理,则会一路沿着调用栈从顶到底抛出,最终终止当前任务的执行;
  // 3.JavaScript线程会继续从任务队列中提取下一个任务继续执行。
  function a(){throw Error("test")}
  function b(){a()}
  b()
  console.log("永远不会执行!")
</script>
<script>
  // 下一个任务
  console.log("你有你抛异常,我照样执行!")
</script>

二.内置异常类型有哪些?

 说到内置异常类那么必先提到的就是Error这个祖先类型了,其他所有的内置异常类和自定义类都必须继承它。而它的标准属性和方法就以下这寥寥几个而已

@prop {String} name - 异常名称
@prop {String} message - 供人类阅读的异常信息
@prop {Function} constructor - 类型构造器
@method toString():String - 输出异常信息

 由于标准属性实在太少,无法提供更有效的信息供开发者定位异常发生的位置和重现事故现场,因此各浏览器厂家均手多多的自己增加些属性,然后逐渐成了事实标准。

@prop {String} fileName - 异常发生的脚本URI
@prop {number} lineNumber - 异常发生的行号
@prop {number} columnNumber - 异常发生的列号
@prop {String} stack - 异常发生时的调用栈信息,IE10及以上才支持
@method toSource():String - 异常发生的脚本内容

另外巨硬还新增以下两个属性

@prop {String} description - 和message差不多
@prop {number} number - 异常类型的编号,巨硬为每个异常设置了一个唯一的编号

 那么现在我要实例化一个Error对象,只需调用Error()new Error()即可;若想同时设置message,则改为Error("test")new Error("test")。其实Error的构造函数签名是这样的

@constructor
@param {String=} message - 设置message属性
@param {String=} fileName - 设置fileName属性
@param {number=} lineNumber - 设置lineNUmber属性

现在我们看看具体有哪些内置的异常类型吧!

  1. EvalError,调用eval()时发生的异常,已被废弃只用于向后兼容而已
  2. InternalError,JavaScript引擎内部异常,FireFox独门提供的!
  3. RangeError,当函数实参越界时发生,如ArrayNumber.toExponential,Number.toFixedNumber.toPrecision时入参非法时。
  4. ReferenceError,当引用未声明的变量时发生
  5. SyntaxError,解析时发生语法错误
  6. TypeError,当值不是所期待的类型时,null.f()也报这个错
  7. URIError,当传递一个非法的URI给全局URI处理函数时发生,如decodeURIComponent('%'),即decodeURIComponentdecodeURI,encodeURIComponentencodeURI

三.动手写自己的异常类型吧!

 关于在StackOverflow上早有人讨论如何自定义异常类型了参考
于是我们顺手拈来即可

function MyError(message, fileName, lineNumber){
  if (this instanceof MyError);else return new MyError(message, fileName, lineNumber)
  this.message = message || ""
  if (fileName){ this.fileName = fileName }
  if (lineNumber){ this.lineNumber = lineNumber }
}

var proto = MyError.prototype = Object.create(Error.prototype)
proto.name = "MyError"
proto.constructor = MyError

cljs实现如下

(defn ^export MyError [& args]
  (this-as this
    (if (instance? MyError this)
      (let [ps ["message" "fileName" "lineNumber"]
            idxs (-> (min (count args) (count ps)) range)]
        (reduce
          (fn [accu i]
            (aset accu (nth ps i) (nth args i))
            accu)
          this
          idxs))
      (apply new MyError args))))

(def proto
  (aset MyError "prototype" (.create js/Object (.-prototype Error))))
(aset proto "name" "MyError")
(aset proto "constructor" MyError)

四.捕获“同步代码”中的"运行时异常",用try/catch就够了

 为了防止由于异常的出现,导致正常代码被略过的风险,我们习惯采取try/catch来捕获并处理异常。

try{
  throw Error("unexpected operation happen...")
}
catch (e){
  console.log(e.message)
}

cljs写法

(try
  (throw (Error. "unexpected operation happen...")
  (catch e
         (println (.-message e)))))

 很多时我们会以为这样书写就万事大吉了,但其实try/catch能且仅能捕获“同步代码”中的"运行时异常"。
1."同步代码"就是说无法获取如setTimeoutPromise等异步代码的异常,也就是说try/catch仅能捕获当前任务的异常,setTimeout等异步代码是在下一个EventLoop中执行。

// 真心捕获不到啊亲~!
try{
  setTimeout(function(){
    throw Error("unexpected operation happen...")
  }, 0)
} catch(e){
  console.log(e)
}

2."运行时异常"是指非SyntaxError,也就是语法错误是无法捕获的,因为在解析JavaScript源码时就报错了,还怎么捕获呢~~

// 非法标识符a->b,真心捕获不到啊亲~!
try{
  a->b = 1
} catch(e){
  console.log(e)
}

 这时大家会急不可待地问:“异步代码的异常咋办呢?语法异常咋办呢?”在解答上述疑问前,我们先偏离一下,稍微挖挖throw语句的特性。

throw后面可以跟什么啊?

 一般而言我们会throw一个Error或其子类的实例(如throw Error()),其实我们throw任何类型的数据(如throw 1,throw "test",throw true等)。但即使可以抛出任意类型的数据,我们还是要坚持抛出Error或其子类的实例。这是为什么呢?

try{
  throw "unexpected operation happen..."
} catch(e){
  console.log(e)
}

try{
  throw TypeError("unexpected operation happen...")
} catch(e){
  if ("TypeError" == e.name){
    // Do something1
  }
  else if ("RangeError" == e.name){
    // Do something2
  }
}

 原因显然易见——异常发生时提供信息越全越好,更容易追踪定位重现问题嘛!

五."万能"异常捕获者window.onerror,真的万能吗?

 在每个可能发生异常的地方都写上try/catch显然是不实际的(另外还存在性能问题),即使是罗嗦如Java我们开发时也就是不断声明throws,然后在顶层处理异常罢了。那么,JavaScript中对应的顶层异常处理入口又在哪呢?木有错,就是在window.onerror。看看方法签名吧

@description window.onerror处理函数
@param {string} message - 异常信息"
@param {string} source  - 发生异常的脚本的URI
@param {number} lineno  - 发生异常的脚本行号
@param {number} colno   - 发生异常的脚本列号
@param {?Error} error   - Error实例,Safari和IE10中没有这个实参

 这时我们就可以通过它捕获除了try/catch能捕获的异常外,还可以捕获setTimeout等的异步代码异常,语法错误。

window.onerror = function(message, source, lineno, colno, error){
  // Do something you like.
}

setTimeout(function(){ throw Error("oh no!") }, 0)
a->b = 1

 这样就满足了吗?还没出大杀技呢——屏蔽异常、屏蔽、屏~~
 只有onerror函数返回true时,异常就不会继续向上抛(否则继续上抛就成了Uncaught Error了)。

// 有异常没问题啊,因为我看不到^_^
window.onerror = function(){return true}

 现在回到标题的疑问中,有了onerror就可以捕获所有异常了吗?答案又是否定的(我的娘啊,还要折腾多久啊~0~)

  1. Chrome中对于跨域脚本所报的异常,虽然onerror能够捕获,但统一报Script Error。若要得到正确的错误信息,则要配置跨域资源共享CORS才可以。
  2. window.onerror实际上采用的事件冒泡的机制捕获异常,并且在冒泡(bubble)阶段时才触发,因此像网络请求异常这些不会冒泡的异常是无法捕获的。
  3. Promise.reject产生的未被catch的异常,window.onerror也是无能为力。

六.Promise.reject也抛异常,怎么办?

 通过Promise来处理复杂的异步流程控制让我们得心应手,但倘若其中出现异常或Promise实例状态变为rejected时,会是怎样一个状况,我们又可以如何处理呢?

Promise是如何标识异常发生的?

 Promise实例的初始化状态是pending,而发生异常时则为rejected,而导致状态从pending转变为rejected的操作有

  1. 调用Promise.reject类方法
  2. 在工厂方法中调用reject方法
  3. 在工厂方法或then回调函数中抛异常
// 方式1
Promise.reject("anything you want")

// 方式2
new Promise(function(resolve, reject) { reject("anything you want") })

// 方式3
new Promise(function{ throw "anything you want" })
new Promise(function(r) { r(Error("anything you want" ) }).then(function(e) { throw e })

 当Promise实例从pending转变为rejected时,和之前谈论到异常一样,要么被捕获处理,要么继续抛出直到成为Uncaught(in promise) Error为止。

异常发生前就catch

 若在异常发生前我们已经调用catch方法来捕获异常,那么则相安无事

new Promise(function(resolve, reject){
  setTimeout(reject, 0)
}).catch(function(e){
  console.log("catch")
  return "bingo"
}).then(function(x){
  console.log(x)
})

// 回显 bingo

专属于Promise的顶层异常处理

 若在异常发生前我们没有调用catch方法来捕获异常,还是可以通过windowunhandledrejection事件捕获异常的

window.addEventListener("unhandledrejection", function(e){
  // Event新增属性
  // @prop {Promise} promise - 状态为rejected的Promise实例
  // @prop {String|Object} reason - 异常信息或rejected的内容

  // 会阻止异常继续抛出,不让Uncaught(in promise) Error产生
  e.preventDefault()
})

迟来的catch

 由于Promise实例可异步订阅其状态变化,也就是可以异步注册catch处理函数,这时其实已经抛出Uncaught(in promise) Error,但我们依然可以处理

var p = new Promise(function(resolve, reject){
  setTimeout(reject, 0)
})
setTimeout(function(){
  p.catch(function(e){
    console.log("catch")
    return "bingo"
  })
}, 1000)

 另外,还可以通过windowrejectionhandled事件监听异步注册catch处理函数的行为

window.addEventListener("rejectionhandled", function(e){
  // Event新增属性
  // @prop {Promise} promise - 状态为rejected的Promise实例
  // @prop {String|Object} reason - 异常信息或rejected的内容

  // Uncaught(in promise) Error已经抛出,所以这句毫无意义^_^
  e.preventDefault()
})

注意:只有抛出Uncaught(in promise) Error后,异步catch才会触发该事件。

七.404等网络请求异常真心要后之后觉吗?

 也许我们都遇到<img src="./404.png">报404网络请求异常的情况,然后测试或用户保障怎么哪个哪个图标没有显示。其实我们我们可以通过以下方式捕获这类异常

window.addEventListener("error", function(e){
  // Do something
  console.log(e.bubbles) // 回显false
}, true)

 由于网络请求异常不会冒泡,因此必须在capture阶段捕获才可以。但还有一个问题是这种方式无法精确判断异常的HTTP状态是404还是500等,因此还是要配合服务端日志来排查分析才可以。

总结

 对异常和如何捕获异常仅仅是前端智能监控中的一小撮知识点,敬请期待后续另一小撮知识点《前端魔法堂——调用栈,异常实例中的宝藏》吧:D
 尊重原创,转载请注明来自:http://www.cnblogs.com/fsjohnhuang/p/7685144.html ^_^肥仔John

参考

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError
https://stackoverflow.com/questions/8504673/how-to-detect-on-page-404-errors-using-javascript

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
8667 0
如何优雅的实现 try catch 异常块?
如何优雅的实现 try catch 异常块?
8 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,大概有三种登录方式:
2852 0
WCF基础教程之异常处理:你的Try..Catch语句真的能捕获到异常吗?
  在上一篇WCF基础教程之开篇:创建、测试和调用WCF博客中,我们简单的介绍了如何创建一个WCF服务并调用这个服务。其实,上一篇博客主要是为了今天这篇博客做铺垫,考虑到网上大多数WCF教程都是从基础讲起的,大家平时工作可能只是去调用和修改WCF的一些方法,而并未创建和配置过WCF,如果大家通过网上的教程去一步一步的创建和配置WCF,中途遇到错误,特别是WCF的配置这块很容易出错,难免会浪费时间。
705 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
10502 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
12313 0
default constructor not found 异常解决方法
default constructor not found 异常解决方法
18 0
JAVA之旅(十)——异常的概述,Try-Catch,异常声明Throws,多异常处理,自定义异常,Throw和Throws的区别
JAVA之旅(十)——异常的概述,Try-Catch,异常声明Throws,多异常处理,自定义异常,Throw和Throws的区别 一.异常的概述 异常算是程序中一个比较重要的环节了,我们首先来看一下异常的体系,我们举一个小例子,定义一个除法方法 //公共的 类 类名 public .
1321 0
Java连接池你用对了吗?一次Unexpected end of stream异常的排查
Apache连接池参数调优、Redis连接关闭机制、Linux网络抓包
647 0
+关注
肥仔john
偏前端的临栈工程师^_^
203
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载