JavaScript错误处理指南

错误是好的。 该材料的作者(我们今天出版的译文)说,他相信这个想法为大家所熟知。 乍一看,这些错误似乎很可怕。 他们可能伴随着某种损失。 在公共场合犯的错误会损害作出这一行为的人的权威。 但是犯错了,我们会从中吸取教训,这意味着下次我们陷入以前行为不正确的情况时,我们会根据需要进行所有操作。



上面我们谈到了人们在日常生活中犯的错误。 编程错误是另外一回事。 错误消息可以帮助我们改进代码,它们可以使我们通知项目的用户出了点问题,并可能告诉用户如何进行操作以使错误不再发生。

此JavaScript错误处理材料分为三个部分。 首先,我们将概述JavaScript中的错误处理系统,并讨论错误对象。 之后,我们寻找解决服务器代码中发生的错误(特别是在使用Node.js + Express.js包时)的问题的答案。 接下来,我们讨论React.js中的错误处理。 之所以选择此处要考虑的框架,是因为它们非常受欢迎。 但是,此处讨论的处理错误的原则是通用的,因此,即使您不使用Express和React,也可以轻松地将学到的知识应用到使用的工具中。

资料库中可以找到资料中使用的演示项目的代码。

1. JavaScript中的错误以及使用它们的通用方法


如果代码中出现问题,则可以使用以下构造。

throw new Error('something went wrong') 

在执行此命令期间,将创建Error对象的实例,并为此对象生成一个异常(或如他们所说的“抛出”)。 throw语句可以引发包含任意表达式的异常。 在这种情况下,如果未采取措施来处理错误,则脚本的执行将停止。

初学者JS程序员通常不使用throw语句。 它们通常会遇到语言运行库或第三方库引发的异常。 发生这种情况时,类似ReferenceError: fs is not defined会进入控制台ReferenceError: fs is not defined ,程序停止执行。

▍对象错误


Error对象的实例具有几个可以使用的属性。 我们感兴趣的第一个属性是message 。 这是获取行的地方,可以将其作为参数传递给错误构造函数。 例如,以下内容显示了如何实例化Error对象并通过访问其构造函数的message属性,将构造函数传递的字符串输出到控制台。

 const myError = new Error('please improve your code') console.log(myError.message) // please improve your code 

对象的第二个属性(非常重要)是错误堆栈的跟踪。 这是一个stack属性。 转到它,您可以查看调用堆栈(错误历史记录),其中显示了导致程序故障的操作顺序。 特别是,这使我们能够了解哪个文件包含错误的代码,并查看哪个函数调用序列导致了错误。 这是通过访问stack属性可以看到的示例。

 Error: please improve your code at Object.<anonymous> (/Users/gisderdube/Documents/_projects/hacking.nosync/error-handling/src/general.js:1:79) at Module._compile (internal/modules/cjs/loader.js:689:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10) at Module.load (internal/modules/cjs/loader.js:599:32) at tryModuleLoad (internal/modules/cjs/loader.js:538:12) at Function.Module._load (internal/modules/cjs/loader.js:530:3) at Function.Module.runMain (internal/modules/cjs/loader.js:742:12) at startup (internal/bootstrap/node.js:266:19) at bootstrapNodeJSCore (internal/bootstrap/node.js:596:3) 

此处,在顶部,有一条错误消息,紧接着是执行引起错误的代码部分的指示,然后描述了调用此失败部分的位置。 相对于错误代码片段,这一直是“最远的”。

▍生成和错误处理


创建Error对象的实例,即执行形式为new Error()的命令不会导致任何特殊后果。 应用throw运算符后,有趣的事情开始发生,这会产生错误。 如前所述,如果未处理此类错误,则脚本执行将停止。 在这种情况下,程序员自己使用throw运算符,某个库或语言运行时(在浏览器或Node.js中)是否发生错误都没有关系。 让我们谈谈各种错误处理方案。

try施工尝试〜抓


try...catch是处理经常被遗忘的错误的最简单方法。 但是,由于它可以用于处理async/await构造中的错误,因此如今它的使用比以前更加密集。

该块可用于处理同步代码中发生的任何错误。 考虑一个例子。

 const a = 5 try {   console.log(b) //  b   -   } catch (err) {   console.error(err) //          } console.log(a) //    ,    

如果在此示例中,我们没有将失败的console.log(b)命令放在try...catch ,则脚本将停止。

▍终于挡


有时,无论是否发生错误,都需要执行一些代码。 为此,可以在try...catch构造中使用第三个(可选) finally块。 通常,它的使用等效于try...catch之后立即出现的某些代码,但是在某些情况下,它可以派上用场。 这是其用法的一个例子。

 const a = 5 try {   console.log(b) //  b   -   } catch (err) {   console.error(err) //          } finally {   console.log(a) //        } 

▍异步机制-回调


使用JavaScript进行编程时,应始终注意异步运行的代码段。 如果您具有异步功能并且其中发生错误,则脚本将继续运行。 当使用回调实现JS中的异步机制时(不建议这样做),相应的回调(回调函数)通常会接收两个参数。 这类似于err参数,可能包含错误和result -带有异步操作的结果。 看起来像这样:

 myAsyncFunc(someInput, (err, result) => {   if(err) return console.error(err) //           console.log(result) }) 

如果回调中发生错误,则该err作为err参数可见。 否则,此参数将获得值undefinednull 。 如果事实证明err某些内容,则必须对此作出响应,因为在我们的示例中,使用return命令,或者使用if...else并将命令放置在else块中以与异步操作的结果一起工作。 关键是要在发生错误的情况下排除使用结果( result参数)的可能性,在这种情况下, result参数可能是undefined 。 例如,如果假定使用此值包含一个对象,则使用此值本身可能会导致错误。 假设当您尝试使用result.data构造或类似的构造时,会发生这种情况。

▍异步机制-承诺


要在JavaScript中执行异步操作,最好使用承诺而不是回调。 在这里,除了提高代码的可读性之外,还有更高级的错误处理机制。 也就是说,您无需担心使用promise时可能属于回调函数的错误对象。 为此,这里提供了一个特殊的挡块。 它拦截在其之前的承诺中发生的所有错误,或在上一个catch之后的代码中发生的所有错误。 请注意,如果在承诺中发生的错误中没有要处理的catch块,这不会停止脚本的执行,但是错误消息不会特别可读。

 (node:7741) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: something went wrong (node:7741) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code. */ 

因此,在处理承诺时,您始终可以建议使用catch 。 看一个例子。

 Promise.resolve(1)   .then(res => {       console.log(res) // 1       throw new Error('something went wrong')       return Promise.resolve(2)   })   .then(res => {       console.log(res) //        })   .catch(err => {       console.error(err) //  ,     ,         return Promise.resolve(3)   })   .then(res => {       console.log(res) // 3   })   .catch(err => {       //      ,      -        console.error(err)   }) 

▍异步机制并尝试...抓住


在JavaScript中出现async/await构造之后,我们返回了处理错误的经典方法- try...catch...finally 。 用这种方法处理错误非常容易和方便。 考虑一个例子。

 ;(async function() {   try {       await someFuncThatThrowsAnError()   } catch (err) {       console.error(err) //       }   console.log('Easy!') //   })() 

使用这种方法,异步代码中的错误以与同步中相同的方式处理。 结果,现在,如果需要,现在可以在单个catch中处理更大范围的错误。

2.生成和处理服务器代码中的错误


现在,我们有了用于处理错误的工具,下面让我们看一下在实际情况下可以如何处理它们。 生成并正确处理错误是服务器端编程的关键方面。 有多种处理错误的方法。 在这里,我们将演示使用我们自己的构造函数来处理Error对象和错误代码的实例的方法,这些实例可以方便地传递到前端或使用服务器API的任何机制。 特定项目的后端的结构并不重要,因为采用任何方法,您都可以使用相同的想法来处理错误。

作为负责路由的服务器框架,我们将使用Express.js。 让我们考虑一下组织有效的错误处理系统所需的结构。 所以这是我们需要的:

  1. 通用错误处理是一种适用于处理任何错误的基本机制,在此期间会出现类似“ Something went wrong, please try again or contact us的消息Something went wrong, please try again or contact us ,要求用户尝试执行失败的操作,然后再次或联系服务器所有者。 该系统不是特别智能,但是至少它能够通知用户出现问题。 这样的消息比“无尽的下载”或类似的消息要好得多。
  2. 处理特定错误-一种机制,可让您通知用户有关系统行为不正确的原因的详细信息,并为他提供有关如何处理问题的具体建议。 例如,这可能涉及用户发送到服务器的请求中缺少一些重要数据,或者数据库中已经存在他正尝试再次添加的特定记录,依此类推。

▍开发自己的错误对象的构造函数


在这里,我们将使用标准的Error类并对其进行扩展。 在JavaScript中使用继承机制是有风险的,但是在这种情况下,这些机制非常有用。 为什么我们需要继承? 事实是,为了便于我们调试代码,我们需要有关错误堆栈跟踪的信息。 扩展了标准的Error类,我们可以轻松跟踪堆栈。 我们将两个属性添加到我们自己的错误对象中。 第一个是code属性,可以使用err.code形式的结构进行访问。 第二个是status属性。 它将记录HTTP状态代码,该状态代码计划传输到应用程序的客户端部分。

这就是CustomError类的样子,其代码被设计为模块。

 class CustomError extends Error {   constructor(code = 'GENERIC', status = 500, ...params) {       super(...params)       if (Error.captureStackTrace) {           Error.captureStackTrace(this, CustomError)       }       this.code = code       this.status = status   } } module.exports = CustomError 

▍路由


现在我们的错误对象已经可以使用了,我们需要配置路由结构。 如上所述,我们需要实现统一的错误处理方法,使我们能够平等地处理所有路由的错误。 默认情况下,Express.js框架不完全支持这种方案。 事实是它的所有路由都被封装。

为了解决这个问题,我们可以实现自己的路由处理程序,并以普通函数的形式定义路由逻辑。 由于这种方法,如果路由功能(或任何其他功能)抛出错误,它将落入路由处理程序,然后可以将其传递给应用程序的客户端。 如果服务器上发生错误,我们计划以以下格式将其传输到前端,假设为此将使用JSON-API:

 {   error: 'SOME_ERROR_CODE',   description: 'Something bad happened. Please try again or contact support.' } 

如果在此阶段发生的事情对您来说令人难以理解-不用担心-继续阅读,尝试使用正在讨论的内容,然后逐渐您会发现它。 实际上,如果我们谈论计算机培训,则在首先讨论一般思想然后进行到具体内容的转换时,这里使用“自上而下”的方法。

这就是路由处理程序代码的样子。

 const express = require('express') const router = express.Router() const CustomError = require('../CustomError') router.use(async (req, res) => {   try {       const route = require(`.${req.path}`)[req.method]       try {           const result = route(req) //    route           res.send(result) //   ,     route       } catch (err) {           /*                ,    route             */           if (err instanceof CustomError) {               /*                   -                                  */               return res.status(err.status).send({                   error: err.code,                   description: err.message,               })           } else {               console.error(err) //                  //   -                   return res.status(500).send({                   error: 'GENERIC',                   description: 'Something went wrong. Please try again or contact support.',               })           }       }   } catch (err) {       /*          ,    ,  ,            ,  ,          ,                       */       res.status(404).send({           error: 'NOT_FOUND',           description: 'The resource you tried to access does not exist.',       })   } }) module.exports = router 

我们相信代码中的注释可以很好地解释它。 我们希望阅读它们比对其后给出的此类代码的解释更为方便。

现在看一下路由文件。

 const CustomError = require('../CustomError') const GET = req => {   //       return { name: 'Rio de Janeiro' } } const POST = req => {   //       throw new Error('Some unexpected error, may also be thrown by a library or the runtime.') } const DELETE = req => {   //  ,      throw new CustomError('CITY_NOT_FOUND', 404, 'The city you are trying to delete could not be found.') } const PATCH = req => {   //      CustomError   try {       //   -        throw new Error('Some internal error')   } catch (err) {       console.error(err) //    ,           throw new CustomError(           'CITY_NOT_EDITABLE',           400,           'The city you are trying to edit is not editable.'       )   } } module.exports = {   GET,   POST,   DELETE,   PATCH, } 

在这些示例中,查询本身不做任何事情。 它只是考虑了错误发生的不同情况。 因此,例如, GET /city请求将属于const GET = req =>...函数const GET = req =>...POST /city请求将属于const POST = req =>...函数const POST = req =>... ,依此类推。 使用查询参数时,此方案也适用。 例如,对于格式为GET /city?startsWith=R的请求, GET /city?startsWith=R 通常,这里已证明,当处理错误时,前端可以获取仅包含重试或与服务器所有者联系的提议的一般错误,也可以使用包含有关问题的详细信息的CustomError构造函数生成的错误。
常规错误数据将以以下形式到达应用程序的客户端部分:

 {   error: 'GENERIC',   description: 'Something went wrong. Please try again or contact support.' } 

CustomError构造函数的用法如下:

 throw new CustomError('MY_CODE', 400, 'Error description') 

这将以下JSON代码传递给前端:

 {   error: 'MY_CODE',   description: 'Error description' } 

既然我们已经在应用程序的服务器部分上进行了充分的工作,那么无用的错误日志将不再属于客户端部分。 相反,客户端会收到有关发生问题的有用信息。

不要忘了, 这里有代码所在的存储库。 您可以下载它,对其进行试验,并在必要时使其适应项目的需求。

3.在客户端上处理错误


现在该描述前端错误处理系统的第三部分了。 在这里,首先有必要处理在应用程序的客户端部分中发生的错误,其次,有必要将在服务器上发生的错误通知用户。 我们将首先处理显示服务器错误信息。 如前所述,React库将在此示例中使用。

error将错误信息保存在应用程序状态


像任何其他数据一样,错误和错误消息可能会更改,因此将它们置于组件状态是有意义的。 当安装组件时,错误数据将被重置,因此,当用户首次看到该页面时,将不会出现错误消息。

接下来要处理的是,必须以相同的样式显示相同类型的错误。 与服务器类似,在这里您可以区分3种类型的错误。

  1. 全局错误-此类包括来自服务器的一般性错误消息,或例如在其他类似情况下用户未登录系统时发生的错误。
  2. 由应用程序的服务器端生成的特定错误-这包括从服务器报告的错误。 例如,如果用户尝试登录并向服务器发送用户名和密码,并且服务器通知他密码不正确,则会发生类似的错误。 在应用程序的客户端部分不会检查这些事情,因此有关此类错误的消息应来自服务器。
  3. 应用程序的客户端部分生成的特定错误。 此类错误的示例是有关在相应字段中输入的无效电子邮件地址的消息。

第二和第三种类型的错误非常相似,您可以使用同一级别组件的状态存储来处理它们。 它们的主要区别在于它们来自不同的来源。 在下面,分析代码,我们将研究与它们一起工作。

它将使用内置系统在React中管理应用程序的状态,但是,如有必要,您可以使用专门的解决方案来管理状态-例如MobX或Redux。

▍全局错误


通常,此类错误消息会以某种状态存储在最高级别的组件中。 它们显示在静态用户界面元素中。 这可以是屏幕顶部的红色框,模式窗口或其他任何内容。 具体实施取决于具体项目。 这就是错误消息的样子。


全局错误信息

现在看一下存储在Application.js文件中的代码。

 import React, { Component } from 'react' import GlobalError from './GlobalError' class Application extends Component {   constructor(props) {       super(props)       this.state = {           error: '',       }       this._resetError = this._resetError.bind(this)       this._setError = this._setError.bind(this)   }   render() {       return (           <div className="container">               <GlobalError error={this.state.error} resetError={this._resetError} />               <h1>Handling Errors</h1>           </div>       )   }   _resetError() {       this.setState({ error: '' })   }   _setError(newError) {       this.setState({ error: newError })   } } export default Application 

, , Application.js , . , .

GlobalError , x , . GlobalError ( GlobalError.js ).

 import React, { Component } from 'react' class GlobalError extends Component {   render() {       if (!this.props.error) return null       return (           <div               style={{                   position: 'fixed',                   top: 0,                   left: '50%',                   transform: 'translateX(-50%)',                   padding: 10,                   backgroundColor: '#ffcccc',                   boxShadow: '0 3px 25px -10px rgba(0,0,0,0.5)',                   display: 'flex',                   alignItems: 'center',               }}           >               {this.props.error}                              <i                   className="material-icons"                   style={{ cursor: 'pointer' }}                   onClick={this.props.resetError}               >                   close               </font></i>           </div>       )   } } export default GlobalError 

if (!this.props.error) return null . , . . , , , . , , x , - , .

, , _setError Application.js . , , , , ( error: 'GENERIC' ). ( GenericErrorReq.js ).

 import React, { Component } from 'react' import axios from 'axios' class GenericErrorReq extends Component {   constructor(props) {       super(props)       this._callBackend = this._callBackend.bind(this)   }   render() {       return (           <div>               <button onClick={this._callBackend}>Click me to call the backend</button>           </div>       )   }   _callBackend() {       axios           .post('/api/city')           .then(result => {               //  -     ,               })           .catch(err => {               if (err.response.data.error === 'GENERIC') {                   this.props.setError(err.response.data.description)               }           })   } } export default GenericErrorReq 

, . , , . . -, , -, UX-, , — .

▍ ,


, , , .




, . . . SpecificErrorReq.js .

 import React, { Component } from 'react' import axios from 'axios' import InlineError from './InlineError' class SpecificErrorRequest extends Component {   constructor(props) {       super(props)       this.state = {           error: '',       }       this._callBackend = this._callBackend.bind(this)   }   render() {       return (           <div>               <button onClick={this._callBackend}>Delete your city</button>               <InlineError error={this.state.error} />           </div>       )   }   _callBackend() {       this.setState({           error: '',       })       axios           .delete('/api/city')           .then(result => {               //  -     ,               })           .catch(err => {               if (err.response.data.error === 'GENERIC') {                   this.props.setError(err.response.data.description)               } else {                   this.setState({                       error: err.response.data.description,                   })               }           })   } } export default SpecificErrorRequest 

, , , x . , , . , , — , , , . , , . , , , — .

▍,


, , , . , , - . .


,

SpecificErrorFrontend.js , .

 import React, { Component } from 'react' import axios from 'axios' import InlineError from './InlineError' class SpecificErrorRequest extends Component {   constructor(props) {       super(props)       this.state = {           error: '',           city: '',       }       this._callBackend = this._callBackend.bind(this)       this._changeCity = this._changeCity.bind(this)   }   render() {       return (           <div>               <input                   type="text"                   value={this.state.city}                   style={{ marginRight: 15 }}                   onChange={this._changeCity}               />               <button onClick={this._callBackend}>Delete your city</button>               <InlineError error={this.state.error} />           </div>       )   }   _changeCity(e) {       this.setState({           error: '',           city: e.target.value,       })   }   _validate() {       if (!this.state.city.length) throw new Error('Please provide a city name.')   }   _callBackend() {       this.setState({           error: '',       })       try {           this._validate()       } catch (err) {           return this.setState({ error: err.message })       }       axios           .delete('/api/city')           .then(result => {               //  -     ,               })           .catch(err => {               if (err.response.data.error === 'GENERIC') {                   this.props.setError(err.response.data.description)               } else {                   this.setState({                       error: err.response.data.description,                   })               }           })   } } export default SpecificErrorRequest 


, , ( GENERIC ), , . , , , , , , , , . .

总结


, , -. console.error(err) , , , . - loglevel .

! ?

Source: https://habr.com/ru/post/zh-CN431078/


All Articles