By Lukas Gisder-Dubé | nov 14, 2018
接著我上一篇文章,我想談?wù)劗惓?。我肯定你之前也聽過——異常是個好東西。一開始,我們害怕異常,畢竟寫bug容易被人噴。其實通過修bug,我們實際上學(xué)會了下次開發(fā)怎么避免這個bug并且可以做得更好。
在生活中,我們常說吃一塹長一智。但對于打代碼來說,有些不一樣。我們的編譯器和一些工具現(xiàn)在都很智能,不但告訴我們哪里出錯,還幫助我們優(yōu)化代碼【譯者:eslint之類的】(有可能還會教我們?nèi)绾涡迯?fù)bug)。
js異常的一般處理方法
throw new Error('something went wrong')
以上代碼將會創(chuàng)建異常實例并停止執(zhí)行你的腳本,除非你在錯誤回調(diào)里做一些處理。當(dāng)你開始了js開發(fā)者的職業(yè)生涯,你自己很可能不會這樣做,但是你會在其它的庫里(或者運行時)看到類似‘ReferenceError: fs為定義’這樣的錯誤。
異常對象
異常對象有兩個屬性供我們使用。第一個是message,是你傳遞給異常的構(gòu)造函數(shù)的參數(shù),比如:
new Error('This is the message')
你可以使用message屬性來訪問到該消息:
const myError = new Error(‘please improve your code’)
console.log(myError.message) // please improve your code
第二個參數(shù)是異常堆棧跟蹤,非常重要。你可以使用stack屬性來訪問。異常堆棧為你提供歷史記錄(調(diào)用堆棧),從中可以查看到哪些文件導(dǎo)致了異常。堆棧頂部也包括了消息,然后是實際的堆棧,從距離異常最近的點開始,然后一直到最外層與異常有關(guān)的文件(譯者:調(diào)用關(guān)系的追溯):
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)
拋出并處理異常
現(xiàn)在單個異常實例沒有任何卵用。例如
new Error('...')
以上代碼并不會觸發(fā)任何東西。當(dāng)異常被拋出,事情就變得有趣了一些。然后,跟上文說的一樣,js引擎停止執(zhí)行你的腳本,除非你做了異常處理。記住,手動拋出異常,還是由庫拋出異常,抑或是運行時拋出異常,都沒關(guān)系。讓我們看看在不同場景如何處理這些異常。
try....catch
這是最簡單的,但是經(jīng)常被忘記的異常處理方法——多虧了async/await,越來越多人使用它了。它可以用來捕捉各種類型的非異步錯誤。例如:
const a = 5
try {
console.log(b) // b is not defined, so throws an error
} catch (err) {
console.error(err) // will log the error with the error stack
}
console.log(a) // still gets executed
如果我們不將console.log(b)包裝在try ... catch塊中,腳本執(zhí)行將停止。
...finally
有時候有不管有沒有異常,都希望執(zhí)行的代碼。你可以使用finally。通常,它與try ... catch語句之后只有一行相同,但有時它可能很有用
const a = 5
try {
console.log(b) // b is not defined, so throws an error
} catch (err) {
console.error(err) // will log the error with the error stack
} finally {
console.log(a) // will always get executed
}
異步——回調(diào)
異步,這是你在使用js時不得不去考慮的一個主題。當(dāng)你有個異步方法,并且改方法內(nèi)部發(fā)生異常時,你的腳本會繼續(xù)執(zhí)行,不會立即出現(xiàn)任何異常。當(dāng)使用回調(diào)來處理異步方法的返回時(順便提一下,不提倡使用回調(diào)),你通常會接收兩個參數(shù),例如:
myAsyncFunc(someInput, (err, result) => {
if(err) return console.error(err) // we will see later what to do with the error object.
console.log(result)
})
如果發(fā)生異常,err參數(shù)就是那個異常。如果沒有,參數(shù)就是undefined或者時null。這樣做很重要,不然如果你試圖訪問result.data時,乳溝發(fā)生異常,得到的結(jié)果就是undefined。
異步——Promises
處理異步的另一種方法時使用promises。除了代碼更易讀,異常處理也改進(jìn)了。我們只需要在catch里處理異常就好了,不需要關(guān)心怎么捕捉異常。當(dāng)鏈?zhǔn)秸{(diào)用promises時,catch會捕獲自promise或最后一個catch塊執(zhí)行以來的所有錯誤。請注意,沒有catch的promises不會終止腳本,但是會降低你的異常信息的可讀性:
(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. */
因此,記得為你的promises加上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) // will not get executed
})
.catch(err => {
console.error(err) // we will see what to do with it later
return Promise.resolve(3)
})
.then(res => {
console.log(res) // 3
})
.catch(err => {
// in case in the previous block occurs another error
console.error(err)
})
try … catch?—?again
隨著js引入async / await,我們回到了處理異常的原始方式,使用try ... catch ... finally,這使得處理它們變得輕而易舉:
;(async function() {
try {
await someFuncThatThrowsAnError()
} catch (err) {
console.error(err) // we will make sense of that later
}
console.log('Easy!') // will get executed
})()
由于這和我們處理“普通”同步異常方式一樣,所以如果有需要,更容易使用更大作用域的catch語句。
服務(wù)器端異常產(chǎn)生與處理
現(xiàn)在我們有處理異常的工具了,讓我們看看在實際情況中可以用這些工具做些什么。異常產(chǎn)生后能在后端正確處理是app的關(guān)鍵部分。這列有幾種處理異常的方法。我將向你展示自定義error構(gòu)造函數(shù)和錯誤代碼的方法,我們可以輕松地將其傳遞給前端或任何API調(diào)用者。構(gòu)建后端的細(xì)節(jié)不重要,基本思路不變。
我們用Express.js作為路由框架。讓我們考慮一下我們希望獲得最有效的異常處理的結(jié)構(gòu)。我們想要:
- 一般異常處理,如某種回退,基本上只是說:“有錯誤,請再試一次或聯(lián)系我們”。這不是特別好,但至少通知了用戶,app出錯了——而不是無限加載或者白屏。
- 特殊錯誤處理為用戶提供詳細(xì)信息,讓用戶了解有什么問題以及如何解決它們,例如,數(shù)據(jù)丟失,已存在條目等等。
構(gòu)建一個自定義error構(gòu)造函數(shù)
我們使用存在的erroe構(gòu)造函數(shù)并且繼承它。繼承在js中是一件危險的事,但是在這里,我覺得非常有用。為什么我們需要它?我們?nèi)匀幌M褩8櫈槲覀兲峁┝己玫恼{(diào)試體驗。拓展js自帶的error構(gòu)造函數(shù)就可以繼續(xù)使用堆棧跟蹤。我們唯一要做的就是添加代碼和傳遞前端error.code
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
如何處理路由
完成error的自定義之后,我們需要設(shè)置路由結(jié)構(gòu)。正如我所指出的,我們需要一個單點truth來進(jìn)行異常處理,這意味著對于每個路由,我們都希望具有相同的異常處理行為。express默認(rèn)是不支持的,因為路由封裝好了。
為了解決這個問題,我們可以實現(xiàn)一個路由處理程序,并把實際的路由邏輯定義為普通函數(shù)。這樣,如果路由功能(或任何內(nèi)部函數(shù))拋出異常,他將返回到路由處理程序,然后可以返回給前端。當(dāng)后端發(fā)生錯誤時,我們用以下格式傳遞給前端——比如一個JSON API:
{
error: 'SOME_ERROR_CODE',
description: 'Something bad happened. Please try again or contact support.'
}
準(zhǔn)備好大吃一驚吧,當(dāng)我說下面這段話時,我的學(xué)生總是生氣:
如果你咋看之下不太理解,不用擔(dān)心。只要使用一段時間,你就會發(fā)現(xiàn)為什么要那樣。
順便說一下,這叫自上而下學(xué)習(xí),我非常喜歡。
路由處理程序像這樣子:
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) // We pass the request to the route function
res.send(result) // We just send to the client what we get returned from the route function
} catch (err) {
/*
This will be entered, if an error occurs inside the route function.
*/
if (err instanceof CustomError) {
/*
In case the error has already been handled, we just transform the error
to our return object.
*/
return res.status(err.status).send({
error: err.code,
description: err.message,
})
} else {
console.error(err) // For debugging reasons
// It would be an unhandled error, here we can just return our generic error object.
return res.status(500).send({
error: 'GENERIC',
description: 'Something went wrong. Please try again or contact support.',
})
}
}
} catch (err) {
/*
This will be entered, if the require fails, meaning there is either
no file with the name of the request path or no exported function
with the given request method.
*/
res.status(404).send({
error: 'NOT_FOUND',
description: 'The resource you tried to access does not exist.',
})
}
})
module.exports = router
我希望你看下代碼的注釋,我想這比我在這解釋有意義?,F(xiàn)在,讓我們看下實際的路由文件長什么樣子:
const CustomError = require('../CustomError')
const GET = req => {
// example for success
return { name: 'Rio de Janeiro' }
}
const POST = req => {
// example for unhandled error
throw new Error('Some unexpected error, may also be thrown by a library or the runtime.')
}
const DELETE = req => {
// example for handled error
throw new CustomError('CITY_NOT_FOUND', 404, 'The city you are trying to delete could not be found.')
}
const PATCH = req => {
// example for catching errors and using a CustomError
try {
// something bad happens here
throw new Error('Some internal error')
} catch (err) {
console.error(err) // decide what you want to do here
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將在第3行結(jié)束,POST / city將在第8行結(jié)束,依此類推。 這也適用于查詢參數(shù),例如 GET / city?startsWith = R. 從本質(zhì)上講,您將有一個未處理的異常,前端將收到:
{
error: 'GENERIC',
description: 'Something went wrong. Please try again or contact support.'
}
或者你手動拋出CustomError,例如:
throw new CustomError('MY_CODE', 400, 'Error description')
上述代碼會變成:
{
error: 'GENERIC',
description: 'Something went wrong. Please try again or contact support.'
}
現(xiàn)在我們有了這個漂亮的后端設(shè)置,我們不再有錯誤日志泄漏到前端,并將始終返回有關(guān)出錯的可用信息。
向用戶顯示異常
下一步也是最后一步是管理前端的異常。 在這里,您希望使用第一部分中描述的工具處理前端邏輯本身產(chǎn)生的異常。但是,也必須顯示來自后端的異常。 我們先來看看我們?nèi)绾物@示異常。 如前所述,我們將在演練中使用React。
把異常保存在react state中
接下來我們要澄清的是具有匹配視覺表示的不同類型的異常。就像在后端一樣,有三種類型:
- 全局異常,例如,其中一個常見的異常是來自后臺,用戶沒有登錄等。
- 來自后臺的具體異常,例如,用戶向后臺發(fā)送登錄憑證。后臺答復(fù)密碼錯誤
- 前端導(dǎo)致的異常,例如,電子郵箱格式錯誤。
2和3雖然源頭不一樣,但是非常類似并且可以在同樣的state處理。我們來看看在代碼中如何實現(xiàn)。
我們使用react原聲state實現(xiàn),但是,你可以使用類似MobX或Redux這樣的狀態(tài)管理系統(tǒng)。
全局異常
通常,我將這些異常保存在最外層的有狀態(tài)組件中并呈現(xiàn)靜態(tài)UI元素,這可能是屏幕頂部的紅色橫幅,模態(tài)或其他任何內(nèi)容,設(shè)計實現(xiàn)你自己決定。
來看下代碼:
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中的狀態(tài)存在異常。我們也有方法重置和更改異常值。 我們將值和重置方法傳遞給GlobalError組件,在點擊‘x’時,該組件會顯示異常并重置。讓我們來看看 我們將值和reset方法向下傳遞給GlobalError組件:
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
</i>
</div>
)
}
}
export default GlobalError
你可以在第五行看到,如果沒有異常,我們不會渲染任何內(nèi)容。這可以防止我們始終在頁面上顯示空的紅色框。當(dāng)然,你可以更改此組件的外觀和行為。例如,你可以使用Timeout替換'x',以便在幾秒鐘后重置異常狀態(tài)。
現(xiàn)在,你已準(zhǔn)備好在任何地方使用此全局異常狀態(tài),只需從Application.js傳遞_setError,然后就可以設(shè)置全局異常,例如 當(dāng)來自后端的請求返回時出現(xiàn)字段error:'GENERIC'。例如:
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 => {
// do something with it, if the request is successful
})
.catch(err => {
if (err.response.data.error === 'GENERIC') {
this.props.setError(err.response.data.description)
}
})
}
}
export default GenericErrorReq
如果你很懶,你可以在這里停下來。即使你有具體異常,也可以隨時更改全局異常狀態(tài)并在頁面頂部顯示錯誤框。但是,本文展示如何處理和顯示特定的異常。為什么?首先,這是處理異常的指南,所以我不能就此止步。其次,如果你把所有異常都作為全局狀態(tài)來顯示,那么UX人員會感到很難受。
處理具體的請求異常
與全局異常類似,我們也可以在其他組件中包含局部異常狀態(tài)。 程序是一樣的:
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 => {
// do something with it, if the request is successful
})
.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
這里要記住的一件事,清除異常通常會有不同的觸發(fā)器。 使用'x'刪除異常是沒有意義的。在這里,在發(fā)出新請求時清除異常會更有意義。你還可以在用戶進(jìn)行更改時清除異常,例如,當(dāng)輸入值改變時。
前端的異常
如前所述,這些異??梢耘c來自后端的特定異常以相同的方式(狀態(tài))處理。 我這次使用帶有輸入字段的示例,只允許用戶刪除城市,當(dāng)他實際提供輸入時:
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 => {
// do something with it, if the request is successful
})
.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
我希望你對如何處理異常有所了解。忘記console.error(錯誤),它是過去的事情了。 可以使用它進(jìn)行調(diào)試,但它不應(yīng)該在生產(chǎn)版本中。 為了防止這種情況,我建議你使用一個日志庫,我過去一直在使用loglevel,我很滿意。