网站源码分享错误检测?网站常见错误代码

老铁们,大家好,相信还有很多朋友对于网站源码分享错误检测和网站常见错误代码的相关问题不太懂,没关系,今天就由我来为大家分享分享网站源码分享错误检测以及网站常见错误代码的问题,文章篇幅可能偏长,希望可以帮助到大家,下面一起来看看吧!

除了调试,处理异常或许是程序员编程时间占比最高的了。我们天天和各种异常打交道,就好像我们天天和Bug打交道一样。因此正确认识异常,并作出合适的异常处理就显得很重要了。

我们先尝试抛开前端这个限定条件,来看下更广泛意义上程序的报错以及异常处理。不管是什么语言,都会有异常的发生。而我们程序员要做的就是正确识别程序中的各种异常,并针对其做相应的异常处理

然而,很多人对异常的处理方式是事后修补,即某个异常发生的时候,增加对应的条件判断,这真的是一种非常低效的开发方式,非常不推荐大家这么做。那么究竟如何正确处理异常呢?由于不同语言有不同的特性,因此异常处理方式也不尽相同。但是异常处理的思维框架一定是一致的。本文就前端异常进行详细阐述,但是读者也可以稍加修改延伸到其他各个领域。

本文讨论的异常指的是软件异常,而非硬件异常。

什么是异常

用直白的话来解释异常的话,就是程序发生了意想不到的情况,这种情况影响到了程序的正确运行

从根本上来说,异常就是一个数据结构,其保存了异常发生的相关信息,比如错误码,错误信息等。以JS中的标准内置对象Error为例,其标准属性有name和message。然而不同的浏览器厂商有自己的自定义属性,这些属性并不通用。比如Mozilla浏览器就增加了filename和stack等属性。

值得注意的是错误只有被抛出,才会产生异常,不被抛出的错误不会产生异常。比如:

functiont(){\nconsole.log(&34;);\nnewError();\nconsole.log(&34;);\n}\nt();

(动画演示)

这段代码不会产生任何的异常,控制台也不会有任何错误输出。

异常的分类

按照产生异常时程序是否正在运行,我们可以将错误分为编译时异常运行时异常

编译时异常指的是源代码在编译成可执行代码之前产生的异常。而运行时异常指的是可执行代码被装载到内存中执行之后产生的异常。

编译时异常

我们知道TS最终会被编译成JS,从而在JSRuntime中执行。既然存在编译,就有可能编译失败,就会有编译时异常。

比如我使用TS写出了如下代码:

consts:string=123;

这很明显是错误的代码,我给s声明了string类型,但是却给它赋值number。

当我使用tsc(typescript编译工具,全称是typescriptcompiler)尝试编译这个文件的时候会有异常抛出:

tsca.ts\na.ts:1:7-errorTS2322:Type&39;isnotassignabletotype&39;.\n\n1consts:string=123;\n~\n\n\nFound1error.

这个异常就是编译时异常,因为我的代码还没有执行。

然而并不是你用了TS才存在编译时异常,JS同样有编译时异常。有的人可能会问JS不是解释性语言么?是边解释边执行,没有编译环节,怎么会有编译时异常?

别急,我举个例子你就明白了。如下代码:

functiont(){\nconsole.log(&39;)\nawaitsa\nconsole.log(&39;)\n}\nt()

上面的代码由于存在语法错误,不会编译通过,因此并不会打印start,侧面证明了这是一个编译时异常。尽管JS是解释语言,也依然存在编译阶段,这是必然的,因此自然也会有编译异常。

总的来说,编译异常可以在代码被编译成最终代码前被发现,因此对我们的伤害更小。接下来,看一下令人心生畏惧的运行时异常

运行时异常

相信大家对运行时异常非常熟悉。这恐怕是广大前端碰到最多的异常类型了。众所周知的NPE(NullPointerException)就是运行时异常。

将上面的例子稍加改造,得到下面代码:

functiont(){\nconsole.log(&34;);\nthrow1;\nconsole.log(&34;);\n}\nt();

(动画演示)

注意end没有打印,并且t没有弹出栈。实际上t最终还是会被弹出的,只不过和普通的返回不一样。

如上,则会打印出start。由于异常是在代码运行过程中抛出的,因此这个异常属于运行时异常。相对于编译时异常,这种异常更加难以发现。上面的例子可能比较简单,但是如果我的异常是隐藏在某一个流程控制语句(比如ifelse)里面呢?程序就可能在客户的电脑走入那个抛出异常的if语句,而在你的电脑走入另一条。这就是著名的《在我电脑上好好的》事件。

异常的传播

异常的传播和我之前写的浏览器事件模型有很大的相似性。只不过那个是作用在DOM这样的数据结构,这个则是作用在函数调用栈这种数据结构,并且事件传播存在捕获阶段,异常传播是没有的。不同C语言,JS中异常传播是自动的,不需要程序员手动地一层层传递。如果一个异常没有被catch,它会沿着函数调用栈一层层传播直到栈空。

异常处理中有两个关键词,它们是throw(抛出异常)catch(处理异常)。当一个异常被抛出的时候,异常的传播就开始了。异常会不断传播直到遇到第一个catch。如果程序员没有手动catch,那么一般而言程序会抛出类似unCaughtError,表示发生了一个异常,并且这个异常没有被程序中的任何catch语言处理。未被捕获的异常通常会被打印在控制台上,里面有详细的堆栈信息,从而帮助程序员快速排查问题。实际上我们的程序的目标是避免unCaughtError这种异常,而不是一般性的异常。

一点小前提

由于JS的Error对象没有code属性,只能根据message来呈现,不是很方便。我这里进行了简单的扩展,后面很多地方我用的都是自己扩展的Error,而不是原生JSError,不再赘述。

oldError=Error;\nError=function({code,message,fileName,lineNumber}){\nerror=newoldError(message,fileName,lineNumber);\nerror.code=code;\nreturnerror;\n};

手动抛出or自动抛出

异常既可以由程序员自己手动抛出,也可以由程序自动抛出。

thrownewError(`I&39;toString&34;Invaliddividend&34;Invaliddivisor&34;foo&34;bar&34;被除数必须是除0之外的数&34;除数必须是数字&34;不可预知的错误&34;Invaliddividend&34;Invaliddivisor&34;foo&34;bar&34;被除数必须是除0之外的数&34;除数必须是数字&34;不可预知的错误&34;foo&34;bar&34;anerroroccured&34;anerroroccured&34;anerroroccured&34;willneverrun&34;anerroroccured&34;NETWORK_ERROR&34;Icanhandlethis&39;thandle,passitdown\nthrowerr;\n}\n}\nfunctionb(){\ntry{\nc();\n}catch(err){\nif(err.code===&34;){\nreturnconsole.log(&34;);\n}\n//can&34;INPUT_ERROR&34;anerroroccured&34;NETWORK_ERROR&34;Icanhandlethis&39;thandle,passitdown\nthrowerr;\n}\n}\nfunctionb(){\ntry{\nc();\n}catch(err){\nif(err.code===&34;){\nreturnconsole.log(&34;);\n}\n}\n}\nfunctionc(){\nthrownewError({\ncode:&34;,\nmessage:&34;,\n});\n}\n\na();

如上代码不会有任何异常被抛出,它被完全吞没了,这对我们调试问题简直是灾难。因此切记不要吞没你不能处理的异常。正确的做法应该是上面讲的那种只catch你可以处理的异常,而将你不能处理的异常throw出来,这就是责任链模式的典型应用。

这只是一个简单的例子,就足以绕半天。实际业务肯定比这个复杂多得多。因此异常处理绝对不是一件容易的事情。

如果说谁来处理是一件困难的事情,那么在异步中决定谁来处理异常就是难上加难,我们来看下。

同步与异步

同步异步一直是前端难以跨越的坎,对于异常处理也是一样。以NodeJS中用的比较多的读取文件API为例。它有两个版本,一个是异步,一个是同步。同步读取仅仅应该被用在没了这个文件无法进行下去的时候。比如读取一个配置文件。而不应该在比如浏览器中读取用户磁盘上的一个图片等,这样会造成主线程阻塞,导致浏览器卡死。

//异步读取文件\nfs.readFileSync();\n//同步读取文件\nfs.readFile();

当我们试图同步读取一个不存在的文件的时候,会抛出以下异常:

fs.readFileSync(&39;);\nconsole.log(&39;);\nThrown:\nError:ENOENT:nosuchfileordirectory,open&39;\natObject.openSync(fs.js:446:3)\natObject.readFileSync(fs.js:348:35){\nerrno:-2,\nsyscall:&39;,\ncode:&39;,\npath:&39;\n}

并且脑洞前端是不会被打印出来的。这个比较好理解,我们上面已经解释过了。

而如果以异步方式的话:

fs.readFile(&39;,(err,data)=>{if(err){throwerr}});\nconsole.log(&39;)\nlucifer\nundefined\nThrown:\n[Error:ENOENT:nosuchfileordirectory,open&39;]{\nerrno:-2,\ncode:&39;,\nsyscall:&39;,\npath:&39;\n}\n>

脑洞前端是会被打印出来的。

其本质在于fs.readFile的函数调用已经成功,并从调用栈返回并执行到下一行的console.log(&39;)。因此错误发生的时候,调用栈是空的,这一点可以从上面的错误堆栈信息中看出来。

不明白为什么调用栈是空的同学可以看下我之前写的《一文看懂浏览器事件循环》

而trycatch的作用仅仅是捕获当前调用栈的错误(上面异常传播部分已经讲过了)。因此异步的错误是无法捕获的,比如;

try{\nfs.readFile(&34;,(err,data)=>{\nif(err){\nthrowerr;\n}\n});\n}catch(err){\nconsole.log(&34;);\n}

上面的catchinganerror不会被打印。因为错误抛出的时候,调用栈中不包含这个catch语句,而仅仅在执行fs.readFile的时候才会。

如果我们换成同步读取文件的例子看看:

try{\nfs.readFileSync(&34;);\n}catch(err){\nconsole.log(&34;);\n}

上面的代码会打印catchinganerror。因为读取文件被同步发起,文件返回之前线程会被挂起,当线程恢复执行的时候,fs.readFileSync仍然在函数调用栈中,因此fs.readFileSync产生的异常会冒泡到catch语句。

简单来说就是异步产生的错误不能用trycatch捕获,而要使用回调捕获。

可能有人会问了,我见过用trycatch捕获异步异常啊。比如:

rejectIn=(ms)=>\nnewPromise((_,r)=>{\nsetTimeout(()=>{\nr(1);\n},ms);\n});\nasyncfunctiont(){\ntry{\nawaitrejectIn(0);\n}catch(err){\nconsole.log(&34;,err);\n}\n}\n\nt();

本质上这只是一个语法糖,是Promise.prototype.catch的一个语法糖而已。而这一语法糖能够成立的原因在于其用了Promise这种包装类型。如果你不用包装类型,比如上面的fs.readFile不用Promise等包装类型包装,打死都不能用trycatch捕获。

而如果我们使用babel转义下,会发现trycatch不见了,变成了switchcase语句。这就是trycatch“可以捕获异步异常”的原因,仅此而已,没有更多。

(babel转义结果)

我使用的babel转义环境都记录在这里,大家可以直接点开链接查看.

虽然浏览器并不像babel转义这般实现,但是至少我们明白了一点。目前的trycatch的作用机制是无法捕获异步异常的。

异步的错误处理推荐使用容器包装,比如Promise。然后使用catch进行处理。实际上Promise的catch和trycatch的catch有很多相似的地方,大家可以类比过去。

和同步处理一样,很多原则都是通用的。比如异步也不要去吞没异常。下面的代码是不好的,因为它吞没了它不能处理的异常。

p=Promise.reject(1);\np.catch(()=>{});

更合适的做法的应该是类似这种:

p=Promise.reject(1);\np.catch((err)=>{\nif(err==1){\nreturnconsole.log(&34;);\n}\nthrowerr;\n});

彻底消除运行时异常可能么?

我个人对目前前端现状最为头疼的一点是:大家过分依赖运行时,而严重忽略编译时。我见过很多程序,你如果不运行,根本不知道程序是怎么走的,每个变量的shape是什么。怪不得处处都可以看到console.log。我相信你一定对此感同身受。也许你就是那个写出这种代码的人,也许你是给别人擦屁股的人。为什么会这样?就是因为大家太依赖运行时。TS的出现很大程度上改善了这一点,前提是你用的是typescript,而不是anyscript。其实eslint以及stylint对此也有贡献,毕竟它们都是静态分析工具。

我强烈建议将异常保留在编译时,而不是运行时。不妨极端一点来看:假如所有的异常都在编译时发生,而一定不会在运行时发生。那么我们是不是就可以信心满满地对应用进行重构啦?

幸运的是,我们能够做到。只不过如果当前语言做不到的话,则需要对现有的语言体系进行改造。这种改造成本真的很大。不仅仅是API,编程模型也发生了翻天覆地的变化,不然函数式也不会这么多年没有得到普及了。

不熟悉函数编程的可以看看我之前写的函数式编程入门篇。

如果才能彻底消除异常呢?在回答这个问题之前,我们先来看下一门号称没有运行时异常的语言elm。elm是一门可以编译为JS的函数式编程语言,其封装了诸如网络IO等副作用,是一种声明式可推导的语言。有趣的是,elm也有异常处理。elm中关于异常处理(ErrorHandling)部分有两个小节的内容,分别是:Maybe和Result。elm之所以没有运行时异常的一个原因就是它们。一句话概括“为什么elm没有异常”的话,那就是elm把异常看作数据(data)

举个简单的例子:

maybeResolveOrNot=(ms)=>\nsetTimeout(()=>{\nif(Math.random()>0.5){\nconsole.log(&34;);\n}else{\nthrownewError(&34;);\n}\n});

上面的代码有一半的可能报错。那么在elm中就不允许这样的情况发生。所有的可能发生异常的代码都会被强制包装一层容器,这个容器在这里是Maybe。

在其他函数式编程语言名字可能有所不同,但是意义相同。实际上,不仅仅是异常,正常的数据也会被包装到容器中,你需要通过容器的接口来获取数据。如果难以理解的话,你可以将其简单理解为Promsie(但并不完全等价)。

Maybe可能返回正常的数据data,也可能会生成一个错误error。某一个时刻只能是其中一个,并且只有运行的时候,我们才真正知道它是什么。从这一点来看,有点像薛定谔的猫。

不过Maybe已经完全考虑到异常的存在,一切都在它的掌握之中。所有的异常都能够在编译时推导出来。当然要想推导出这些东西,你需要对整个编程模型做一定的封装会抽象,比如DOM就不能直接用了,而是需要一个中间层。

再来看下一个更普遍的例子NPE:

null.toString();

elm也不会发生。原因也很简单,因为null也会被包装起来,当你通过这个包装类型就行访问的时候,容器有能力避免这种情况,因此就可以不会发生异常。当然这里有一个很重要的前提就是可推导,而这正是函数式编程语言的特性。这部分内容超出了本文的讨论范围,不再这里说了。

运行时异常可以恢复么?

最后要讨论的一个主题是运行时异常是否可以恢复。先来解释一下,什么是运行时异常的恢复。还是用上面的例子:

functiont(){\nconsole.log(&34;);\nthrow1;\nconsole.log(&34;);\n}\nt();

这个我们已经知道了,end是不会打印的。尽管你这么写也是无济于事:

functiont(){\ntry{\nconsole.log(&34;);\nthrow1;\nconsole.log(&34;);\n}catch(err){\nconsole.log(&34;);\n}\n}\nt();

如果我想让它打印呢?我想让程序面对异常可以自己recover怎么办?我已经捕获这个错误,并且我确信我可以处理,让流程继续走下去吧!如果有能力做到这个,这个就是运行时异常恢复

遗憾地告诉你,据我所知,目前没有任何一个引擎能够做到这一点。

这个例子过于简单,只能帮助我们理解什么是运行时异常恢复,但是不足以让我们看出这有什么用?

我们来看一个更加复杂的例子,我们这里直接使用上面实现过的函数divide。

functiont(){\ntry{\nconstres=divide(&34;,&34;);\nalert(`yougot${res}`);\n}catch(err){\nif(err.code===1){\nreturnconsole.log(&34;);\n}\nif(err.code===2){\nreturnconsole.log(&34;);\n}\nthrownewError(&34;);\n}\n}

如上代码,会进入catch,而不会alert。因此对于用户来说,应用程序是没有任何响应的。这是不可接受的。

要吐槽一点的是这种事情真的是挺常见的,只不过大家用的不是alert罢了。

如果我们的代码在进入catch之后还能够继续返回出错位置继续执行就好了。

如何实现异常中断的恢复呢?我刚刚说了:据我所知,目前没有任何一个引擎能够做到异常恢复。那么我就来发明一个新的语法解决这个问题。

functiont(){\ntry{\nconstres=divide(&34;,&34;);\nalert(`yougot${res}`);\n}catch(err){\nconsole.log(&34;);\nresume-1;\n}\n}\nt();

上面的resume是我定义的一个关键字,功能是如果遇到异常,则返回到异常发生的地方,然后给当前发生异常的函数一个返回值-1,并使得后续代码能够正常运行,不受影响。这其实是一种fallback。

这绝对是一个超前的理念。当然挑战也非常大,对现有的体系冲击很大,很多东西都要改。我希望社区可以考虑把这个东西加到标准。

最佳实践

通过前面的学习,你已经知道了异常是什么,异常是怎么产生的,以及如何正确处理异常(同步和异步)。接下来,我们谈一下异常处理的最佳实践。

我们平时开发一个应用。如果站在生产者和消费者的角度来看的话。当我们使用别人封装的框架,库,模块,甚至是函数的时候,我们就是消费者。而当我们写的东西被他人使用的时候,我们就是生产者。

实际上,就算是生产者内部也会有多个模块构成,多个模块之间也会有生产者和消费者的再次身份转化。不过为了简单起见,本文不考虑这种关系。这里的生产者指的就是给他人使用的功能,是纯粹的生产者。

从这个角度出发,来看下异常处理的最佳实践。

作为消费者

当作为消费者的时候,我们关心的是使用的功能是否会抛出异常,如果是,他们有哪些异常。比如:

importfoofrom&34;;\ntry{\nfoo.bar();\n}catch(err){\n//有哪些异常?\n}

当然,理论上foo.bar可能产生任何异常,而不管它的API是这么写的。但是我们关心的是可预期的异常。因此你一定希望这个时候有一个API文档,详细列举了这个API可能产生的异常有哪些。

比如这个foo.bar4种可能的异常分别是A,B,C和D。其中A和B是我可以处理的,而C和D是我不能处理的。那么我应该:

importfoofrom&34;;\ntry{\nfoo.bar();\n}catch(err){\nif(err.code===&34;){\nreturnconsole.log(&34;);\n}\nif(err.code===&34;){\nreturnconsole.log(&34;);\n}\nthrowerr;\n}

可以看出,不管是C和D,还是API中没有列举的各种可能异常,我们的做法都是直接抛出。

作为生产者

如果你作为生产者,你要做的就是提供上面提到的详细的API,告诉消费者你的可能错误有哪些。这样消费者就可以在catch中进行相应判断,处理异常情况。

你可以提供类似上图的错误表,让大家可以很快知道可能存在的可预知异常有哪些。不得不吐槽一句,在这一方面很多框架,库做的都很差。希望大家可以重视起来,努力维护良好的前端开发大环境。

总结

本文很长,如果你能耐心看完,你真得给可以给自己鼓个掌。

我从什么是异常,以及异常的分类,让大家正确认识异常,简单来说异常就是一种数据结构而已。

接着,我又讲到了异常的传播和处理。这两个部分是紧密联系的。异常的传播和事件传播没有本质不同,主要不同是数据结构不同,思想是类似的。具体来说异常会从发生错误的调用处,沿着调用栈回退,直到第一个catch语句或者栈为空。如果栈为空都没有碰到一个catch,则会抛出uncaughtError。需要特别注意的是异步的异常处理,不过你如果对我讲的原理了解了,这都不是事。

然后,我提出了两个脑洞问题:

彻底消除运行时异常可能么?运行时异常可以恢复么?

这两个问题非常值得研究,但由于篇幅原因,我这里只是给你讲个轮廓而已。如果你对这两个话题感兴趣,可以和我交流。

最后,我提到了前端异常处理的最佳实践。大家通过两种角色(生产者和消费者)的转换,认识一下不同决定关注点以及承担责任的不同。具体来说提到了明确声明可能的异常以及处理你应该处理的,不要吞没你不能处理的异常。当然这个最佳实践仍然是轮廓性的。如果大家想要一份前端最佳实践checklist,可以给我留言。留言人数较多的话,我考虑专门写一个前端最佳实践checklist类型的文章。

大家也可以关注我的公众号《脑洞前端》获取更多更新鲜的前端硬核文章,带你认识你不知道的前端。

网站源码分享错误检测的介绍就聊到这里吧,感谢你花时间阅读本站内容,更多关于网站常见错误代码、网站源码分享错误检测的信息别忘了在本站进行查找哦。

Published by

风君子

独自遨游何稽首 揭天掀地慰生平