热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

php该throw还是该404,前端魔法堂——异常不仅仅是try/catch

前言编程时我们往往拿到的是业务流程正确的业务说明文档或规范,但实际开发中却布满荆棘和例外情况,而这些例外中包含业务用例的例外,也包含技术上

前言

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

我打算分成《前端魔法堂——异常不仅仅是try/catch》和《前端魔法堂——调用栈,异常实例中的宝藏》两篇分别叙述内置/自定义异常类,捕获运行时异常/语法异常/网络请求异常/PromiseRejection事件,什么是调用栈和如何获取调用栈的相关信息。

是不是未出发就已经很期待呢?好吧,大家捉紧扶手,老司机要开车了^_^

概要

本篇将叙述如下内容:

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

在学习Java时我们会被告知异常(Exception)和错误(Error)是不一样的,异常是不会导致进程终止从而可以被修复(try/catch),但错误将会导致进程终止因此不能被修复。当对于Javascript而言,我们要面对的仅仅有异常(虽然异常类名为Error或含Error字样),异常的出现不会导致Javascript引擎崩溃,最多就是让当前执行的任务终止而已。

上面说到异常的出现最多就是让当前执行的任务终止,到底是什么意思呢?这里就涉及到Event Loop的原理了,下面我尝试用代码大致说明吧。

// 1.当前代码块将作为一个任务压入任务队列中,Javascript线程会不断地从任务队列中提取任务执行;

// 2.当任务执行过程中报异常,且异常没有捕获处理,则会一路沿着调用栈从顶到底抛出,最终终止当前任务的执行;

// 3.Javascript线程会继续从任务队列中提取下一个任务继续执行。

function a(){throw Error("test")}

function b(){a()}

b()

console.log("永远不会执行!")

// 下一个任务

console.log("你有你抛异常,我照样执行!")

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

说到内置异常类那么必先提到的就是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属性

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

EvalError,调用eval()时发生的异常,已被废弃只用于向后兼容而已

InternalError,Javascript引擎内部异常,FireFox独门提供的!

RangeError,当函数实参越界时发生,如Array,Number.toExponential,Number.toFixed和Number.toPrecision时入参非法时。

ReferenceError,当引用未声明的变量时发生

SyntaxError,解析时发生语法错误

TypeError,当值不是所期待的类型时,null.f()也报这个错

URIError,当传递一个非法的URI给全局URI处理函数时发生,如decodeURIComponent('%'),即decodeURIComponent,decodeURI,encodeURIComponent,encodeURI

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

关于在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."同步代码"就是说无法获取如setTimeout、Promise等异步代码的异常,也就是说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~)

Chrome中对于跨域脚本所报的异常,虽然onerror能够捕获,但统一报Script Error。若要得到正确的错误信息,则要配置跨域资源共享CORS才可以。

window.onerror实际上采用的事件冒泡的机制捕获异常,并且在冒泡(bubble)阶段时才触发,因此像网络请求异常这些不会冒泡的异常是无法捕获的。

Promise.reject产生的未被catch的异常,window.onerror也是无能为力。

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

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

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

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

调用Promise.reject类方法

在工厂方法中调用reject方法

在工厂方法或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方法来捕获异常,还是可以通过window的unhandledrejection事件捕获异常的

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)

另外,还可以通过window的rejectionhandled事件监听异步注册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等网络请求异常真心要后之后觉吗?

也许我们都遇到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/fsjohn... ^_^肥仔John

参考



推荐阅读
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • 也就是|小窗_卷积的特征提取与参数计算
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了卷积的特征提取与参数计算相关的知识,希望对你有一定的参考价值。Dense和Conv2D根本区别在于,Den ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • WPF之Binding初探
      初学wpf,经常被Binding搞晕,以下记录写Binding的基础。首先,盗用张图。这图形象的说明了Binding的机理。对于Binding,意思是数据绑定,基本用法是:1、 ... [详细]
  • 本文由编程笔记#小编为大家整理,主要介绍了logistic回归(线性和非线性)相关的知识,包括线性logistic回归的代码和数据集的分布情况。希望对你有一定的参考价值。 ... [详细]
  • 本文介绍了如何使用PHP向系统日历中添加事件的方法,通过使用PHP技术可以实现自动添加事件的功能,从而实现全局通知系统和迅速记录工具的自动化。同时还提到了系统exchange自带的日历具有同步感的特点,以及使用web技术实现自动添加事件的优势。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • Metasploit攻击渗透实践
    本文介绍了Metasploit攻击渗透实践的内容和要求,包括主动攻击、针对浏览器和客户端的攻击,以及成功应用辅助模块的实践过程。其中涉及使用Hydra在不知道密码的情况下攻击metsploit2靶机获取密码,以及攻击浏览器中的tomcat服务的具体步骤。同时还讲解了爆破密码的方法和设置攻击目标主机的相关参数。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • MySQL数据库锁机制及其应用(数据库锁的概念)
    本文介绍了MySQL数据库锁机制及其应用。数据库锁是计算机协调多个进程或线程并发访问某一资源的机制,在数据库中,数据是一种供许多用户共享的资源,如何保证数据并发访问的一致性和有效性是数据库必须解决的问题。MySQL的锁机制相对简单,不同的存储引擎支持不同的锁机制,主要包括表级锁、行级锁和页面锁。本文详细介绍了MySQL表级锁的锁模式和特点,以及行级锁和页面锁的特点和应用场景。同时还讨论了锁冲突对数据库并发访问性能的影响。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • 本文整理了常用的CSS属性及用法,包括背景属性、边框属性、尺寸属性、可伸缩框属性、字体属性和文本属性等,方便开发者查阅和使用。 ... [详细]
author-avatar
非常名花有主
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有