By Lukas Gisder-Dubé | nov 14, 2018
接著我上一篇文章,我想談談異常。我肯定你之前也聽過——異常是個好東西。一開始,我們害怕異常,畢竟寫bug容易被人噴。其實通過修bug,我們實際上學會了下次開發怎麼避免這個bug並且可以做得更好。
在生活中,我們常說吃一塹長一智。但對於打程式碼來說,有些不一樣。我們的編譯器和一些工具現在都很智慧,不但告訴我們哪裡出錯,還幫助我們優化程式碼【譯者:eslint之類的】(有可能還會教我們如何修復bug)。
js異常的一般處理方法
throw new Error('something went wrong')複製程式碼
以上程式碼將會建立異常例項並停止執行你的指令碼,除非你在錯誤回撥裡做一些處理。當你開始了js開發者的職業生涯,你自己很可能不會這樣做,但是你會在其它的庫裡(或者執行時)看到類似‘ReferenceError: fs為定義’這樣的錯誤。
異常物件
異常物件有兩個屬性供我們使用。第一個是message,是你傳遞給異常的建構函式的引數,比如:
new Error('This is the message')複製程式碼
你可以使用message屬性來訪問到該訊息:
const myError = new Error(‘please improve your code’)console.log(myError.message) // please improve your code複製程式碼
第二個引數是異常堆疊跟蹤,非常重要。你可以使用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)複製程式碼
丟擲並處理異常
現在單個異常例項沒有任何卵用。例如
new Error('...')複製程式碼
以上程式碼並不會觸發任何東西。當異常被丟擲,事情就變得有趣了一些。然後,跟上文說的一樣,js引擎停止執行你的指令碼,除非你做了異常處理。記住,手動丟擲異常,還是由庫丟擲異常,抑或是執行時丟擲異常,都沒關係。讓我們看看在不同場景如何處理這些異常。
try….catch
這是最簡單的,但是經常被忘記的異常處理方法——多虧了async/await,越來越多人使用它了。它可以用來捕捉各種型別的非非同步錯誤。例如:
const a = 5try {
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塊中,指令碼執行將停止。
…finally
有時候有不管有沒有異常,都希望執行的程式碼。你可以使用finally。通常,它與try … catch語句之後只有一行相同,但有時它可能很有用
const a = 5try {
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
}複製程式碼
非同步——回撥
非同步,這是你在使用js時不得不去考慮的一個主題。當你有個非同步方法,並且改方法內部發生異常時,你的指令碼會繼續執行,不會立即出現任何異常。當使用回撥來處理非同步方法的返回時(順便提一下,不提倡使用回撥),你通常會接收兩個引數,例如:
myAsyncFunc(someInput, (err, result) =>
{
if(err) return console.error(err) // we will see later what to do with the error object. console.log(result)
})複製程式碼
如果發生異常,err引數就是那個異常。如果沒有,引數就是undefined或者時null。這樣做很重要,不然如果你試圖訪問result.data時,乳溝發生異常,得到的結果就是undefined。
非同步——Promises
處理非同步的另一種方法時使用promises。除了程式碼更易讀,異常處理也改進了。我們只需要在catch裡處理異常就好了,不需要關心怎麼捕捉異常。當鏈式呼叫promises時,catch會捕獲自promise或最後一個catch塊執行以來的所有錯誤。請注意,沒有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語句。
伺服器端異常產生與處理
現在我們有處理異常的工具了,讓我們看看在實際情況中可以用這些工具做些什麼。異常產生後能在後端正確處理是app的關鍵部分。這列有幾種處理異常的方法。我將向你展示自定義error建構函式和錯誤程式碼的方法,我們可以輕鬆地將其傳遞給前端或任何API呼叫者。構建後端的細節不重要,基本思路不變。
我們用Express.js作為路由框架。讓我們考慮一下我們希望獲得最有效的異常處理的結構。我們想要:
- 一般異常處理,如某種回退,基本上只是說:“有錯誤,請再試一次或聯絡我們”。這不是特別好,但至少通知了使用者,app出錯了——而不是無限載入或者白屏。
- 特殊錯誤處理為使用者提供詳細資訊,讓使用者瞭解有什麼問題以及如何解決它們,例如,資料丟失,已存在條目等等。
構建一個自定義error建構函式
我們使用存在的erroe建構函式並且繼承它。繼承在js中是一件危險的事,但是在這裡,我覺得非常有用。為什麼我們需要它?我們仍然希望堆疊跟蹤為我們提供良好的除錯體驗。擴充js自帶的error建構函式就可以繼續使用堆疊跟蹤。我們唯一要做的就是新增程式碼和傳遞前端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的自定義之後,我們需要設定路由結構。正如我所指出的,我們需要一個單點truth來進行異常處理,這意味著對於每個路由,我們都希望具有相同的異常處理行為。express預設是不支援的,因為路由封裝好了。
為了解決這個問題,我們可以實現一個路由處理程式,並把實際的路由邏輯定義為普通函式。這樣,如果路由功能(或任何內部函式)丟擲異常,他將返回到路由處理程式,然後可以返回給前端。當後端發生錯誤時,我們用以下格式傳遞給前端——比如一個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) // 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複製程式碼
我希望你看下程式碼的註釋,我想這比我在這解釋有意義。現在,讓我們看下實際的路由檔案長什麼樣子:
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行結束,POST / city將在第8行結束,依此類推。 這也適用於查詢引數,例如 GET / city?startsWith = R. 從本質上講,您將有一個未處理的異常,前端將收到:
{
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.'
}複製程式碼
現在我們有了這個漂亮的後端設定,我們不再有錯誤日誌洩漏到前端,並將始終返回有關出錯的可用資訊。
向使用者顯示異常
下一步也是最後一步是管理前端的異常。 在這裡,您希望使用第一部分中描述的工具處理前端邏輯本身產生的異常。但是,也必須顯示來自後端的異常。 我們先來看看我們如何顯示異常。 如前所述,我們將在演練中使用React。
把異常儲存在react state中
接下來我們要澄清的是具有匹配視覺表示的不同型別的異常。就像在後端一樣,有三種型別:
- 全域性異常,例如,其中一個常見的異常是來自後臺,使用者沒有登入等。
- 來自後臺的具體異常,例如,使用者向後臺傳送登入憑證。後臺答覆密碼錯誤
- 前端導致的異常,例如,電子郵箱格式錯誤。
2和3雖然源頭不一樣,但是非常類似並且可以在同樣的state處理。我們來看看在程式碼中如何實現。
我們使用react原聲state實現,但是,你可以使用類似MobX或Redux這樣的狀態管理系統。
全域性異常
通常,我將這些異常儲存在最外層的有狀態元件中並呈現靜態UI元素,這可能是螢幕頂部的紅色橫幅,模態或其他任何內容,設計實現你自己決定。
來看下程式碼:
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’時,該元件會顯示異常並重置。讓我們來看看 我們將值和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
} &
nbsp;
<
i className="material-icons" style={{
cursor: 'pointer'
}
} onClick={this.props.resetError
} >
close <
/i>
<
/div>
)
}
}export default GlobalError複製程式碼
你可以在第五行看到,如果沒有異常,我們不會渲染任何內容。這可以防止我們始終在頁面上顯示空的紅色框。當然,你可以更改此元件的外觀和行為。例如,你可以使用Timeout替換’x’,以便在幾秒鐘後重置異常狀態。
現在,你已準備好在任何地方使用此全域性異常狀態,只需從Application.js傳遞_setError,然後就可以設定全域性異常,例如 當來自後端的請求返回時出現欄位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複製程式碼
如果你很懶,你可以在這裡停下來。即使你有具體異常,也可以隨時更改全域性異常狀態並在頁面頂部顯示錯誤框。但是,本文展示如何處理和顯示特定的異常。為什麼?首先,這是處理異常的指南,所以我不能就此止步。其次,如果你把所有異常都作為全域性狀態來顯示,那麼UX人員會感到很難受。
處理具體的請求異常
與全域性異常類似,我們也可以在其他元件中包含區域性異常狀態。 程式是一樣的:
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複製程式碼
這裡要記住的一件事,清除異常通常會有不同的觸發器。 使用’x’刪除異常是沒有意義的。在這裡,在發出新請求時清除異常會更有意義。你還可以在使用者進行更改時清除異常,例如,當輸入值改變時。
前端的異常
如前所述,這些異常可以與來自後端的特定異常以相同的方式(狀態)處理。 我這次使用帶有輸入欄位的示例,只允許使用者刪除城市,當他實際提供輸入時:
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(錯誤),它是過去的事情了。 可以使用它進行除錯,但它不應該在生產版本中。 為了防止這種情況,我建議你使用一個日誌庫,我過去一直在使用loglevel,挺不錯的。