js錯誤處理權威指北

By Lukas Gisder-Dubé | nov 14, 2018

原文javascript

接着我上一篇文章,我想談談異常。我確定你以前也聽過——異常是個好東西。一開始,咱們懼怕異常,畢竟寫bug容易被人噴。其實經過修bug,咱們實際上學會了下次開發怎麼避免這個bug而且能夠作得更好。前端

在生活中,咱們常說吃一塹長一智。但對於打代碼來講,有些不同。咱們的編譯器和一些工具如今都很智能,不但告訴咱們哪裏出錯,還幫助咱們優化代碼【譯者:eslint之類的】(有可能還會教咱們如何修復bug)。java

js異常的通常處理方法

throw new Error('something went wrong')

以上代碼將會建立異常實例並中止執行你的腳本,除非你在錯誤回調裏作一些處理。當你開始了js開發者的職業生涯,你本身極可能不會這樣作,可是你會在其它的庫裏(或者運行時)看到相似‘ReferenceError: fs爲定義’這樣的錯誤。node

異常對象

異常對象有兩個屬性供咱們使用。第一個是message,是你傳遞給異常的構造函數的參數,好比:react

new Error('This is the message')

你可使用message屬性來訪問到該消息:ios

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

第二個參數是異常堆棧跟蹤,很是重要。你可使用stack屬性來訪問。異常堆棧爲你提供歷史記錄(調用堆棧),從中能夠查看到哪些文件致使了異常。堆棧頂部也包括了消息,而後是實際的堆棧,從距離異常最近的點開始,而後一直到最外層與異常有關的文件(譯者:調用關係的追溯):git

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)

拋出並處理異常

如今單個異常實例沒有任何卵用。例如express

new Error('...')

以上代碼並不會觸發任何東西。當異常被拋出,事情就變得有趣了一些。而後,跟上文說的同樣,js引擎中止執行你的腳本,除非你作了異常處理。記住,手動拋出異常,仍是由庫拋出異常,抑或是運行時拋出異常,都不要緊。讓咱們看看在不一樣場景如何處理這些異常。bootstrap

try....catch

這是最簡單的,可是常常被忘記的異常處理方法——多虧了async/await,愈來愈多人使用它了。它能夠用來捕捉各類類型的非異步錯誤。例如:axios

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塊中,腳本執行將中止。

...finally

有時候有無論有沒有異常,都但願執行的代碼。你可使用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
}

異步——回調

異步,這是你在使用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做爲路由框架。讓咱們考慮一下咱們但願得到最有效的異常處理的結構。咱們想要:

  1. 通常異常處理,如某種回退,基本上只是說:「有錯誤,請再試一次或聯繫咱們」。這不是特別好,但至少通知了用戶,app出錯了——而不是無限加載或者白屏。
  2. 特殊錯誤處理爲用戶提供詳細信息,讓用戶瞭解有什麼問題以及如何解決它們,例如,數據丟失,已存在條目等等。

構建一個自定義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中

接下來咱們要澄清的是具備匹配視覺表示的不一樣類型的異常。就像在後端同樣,有三種類型:

  1. 全局異常,例如,其中一個常見的異常是來自後臺,用戶沒有登陸等。
  2. 來自後臺的具體異常,例如,用戶向後臺發送登陸憑證。後臺答覆密碼錯誤
  3. 前端致使的異常,例如,電子郵箱格式錯誤。

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,我很滿意。

相關文章
相關標籤/搜索