錯誤處理很重要。
簡述
這篇可以當作是從 JavaScript-處理錯誤的方法 延伸出來的筆記,所以會假設你已經知道下面這幾個概念:
- 知道
try...catch..finally
的流程
- 知道
throw new Error()
是什麼?跟會發生什麼事?
- 知道
Error
物件是什麼?還有 Error
的類別
總之這篇只是想舉幾個比較特別的範例,所以基本概念不會講太多。
案例一:當頁面載入時有很多資料要初始化
這應該是蠻常見的案例,以我在工作的經驗來說,就有碰到下拉選單項目是透過 API 拿到的資料來產生的,所以 useEffect
中就會有一大堆 API 要打,像這樣:
1 2 3 4 5 6 7
| useEffect(() => { getDropdown1.then((response) => setDropdownList1(response)) getDropdown2.then((response) => setDropdownList2(response)) getDropdown3.then((response) => setDropdownList3(response)) getDropdown4.then((response) => setDropdownList4(response)) ... }, [])
|
這種時候你應該就會想要把這些東西包成一個 function 來處理,不然要處理錯誤的話很麻煩,會變成這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| useEffect(() => { getDropdown1 .then((response) => setDropdownList1(response)) .catch((error) => setError(error.message)) getDropdown2 .then((response) => setDropdownList2(response)) .catch((error) => setError(error.message)) getDropdown3 .then((response) => setDropdownList3(response)) .catch((error) => setError(error.message)) getDropdown4 .then((response) => setDropdownList4(response)) .catch((error) => setError(error.message)) }, [])
|
所以就要透過 function 來包裝會更簡潔一點。不過在那之前我想先強調一個很重要的觀念:
catch
要留到最外層來做
catch
要留到最外層來做
catch
要留到最外層來做
什麼意思?我們先假設最後包裝好的結果會像這樣:
1 2 3 4 5
| useEffect(() => { getInitDropdown() .then(() => console.log('finish')) .catch((error) => setError(error.message)) }, [])
|
如果你要把 error 留到這邊來處理,那麼在 getInitDropdown
中就不可以出現任何 catch
。
下面是錯誤的範例:
附註:加上 async
後最後才會回傳 Promise
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| async function getInitDropdown() { await getDropdown1 .then((response) => setDropdownList1(response)) .catch((error) => setError(error.message)) await getDropdown2 .then((response) => setDropdownList2(response)) .catch((error) => setError(error.message)) await getDropdown3 .then((response) => setDropdownList3(response)) .catch((error) => setError(error.message)) await getDropdown4 .then((response) => setDropdownList4(response)) .catch((error) => setError(error.message)) }
|
如果是這樣寫,當任何一個地方出現 Error 時就會被它最靠近的 catch 給接住,這時候外面的 getInitDropdown
就沒辦法 catch
到,因為已經先被裡面的 function 被接走了。
可以參考 PJCHENder 說的這一段:
當 JavaScript 程式執行的過程中發生錯誤時,它會丟出例外狀況(throw an exception),JS 並不會繼續往下走,而是尋找有沒有任何程式碼能夠處理這些錯誤(exception handling code),如果沒有找到任何可以處理錯誤狀況的程式碼,則它會從丟出例外狀況的函式中跳出(return),就這樣重複尋找錯誤處理的程式碼、跳出,直到觸及到最外層的函式(top level function)後終止。
這個就是 JS 裡 catch
的機制,一定一定要搞清楚這一段。
所以呢,除非你有要針對特定的 getDropdown
做額外處理,不然請一律不要加上任何 catch
,留給最外層來做就好:
1 2 3 4 5 6
| async function getInitDropdown() { await getDropdown1.then((response) => setDropdownList1(response)) await getDropdown2.then((response) => setDropdownList2(response)) await getDropdown3.then((response) => setDropdownList3(response)) await getDropdown4.then((response) => setDropdownList4(response)) }
|
改成這樣以後,代表只要有任何一個地方出錯就會立刻 return 出去,然後進入 getInitDropdown
的 catch
區塊。
案例二:我想在 custom hook 中 catch 錯誤,但也希望元件能夠 catch 到。
你應該會覺得我在公 three 小,不過這確實是我當初碰到的一個問題,所以我會邊舉例邊說明。
首先我們要把 這個表單範例 寫成一個 custom hook,寫完後會像這樣來用它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import './styles.css' import { useForm } from './useForm'
export default function App() { const { name, job, updateJob, updateName, addNewUser, error, loading } = useForm()
return ( <div className='container'> <h1>Catch Error at custome hook</h1> <form onSubmit={addNewUser}> {error && <div className='error'>{error}</div>} <div className='label'>Name: </div> <input type='text' value={name} onChange={updateName} /> <div className='label'>Job: </div> <input type='text' value={job} onChange={updateJob} /> <div className='btn-wrap'> <button>{loading ? 'sending...' : 'submit'}</button> </div> </form> </div> ) }
|
這個 useForm
做的事情有幾個:
- 有 Error 時顯示錯誤訊息
- 在 Loading 時顯示讀取狀態
- 把 input 值跟 state 做綁定
- 表單送出時執行
addNewUser
發出 request
好,假設我們現在希望 addNewUser
完之後做一個「redirect」的動作的話,怎麼寫?
這是 addNewUser
目前的邏輯:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const addNewUser = (e) => { e.preventDefault() setLoading(true) fetch(url, { method: 'post', body: JSON.stringify({ name, job }) }) .then(() => { console.log('add succuess') setLoading(false) }) .catch((error) => { setLoading(false) setError(error.message) }) }
|
雖然最簡單的方法是直接把 redirect 的邏輯直接寫在裡面,但考慮到「重用性」的話不會這樣做。
所以比較好的改法是加上 async
,讓 addNewUser
結束後可以被下一個 then
接下去:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const addNewUser = async (e) => { e.preventDefault() setLoading(true) await fetch(url, { method: 'post', body: JSON.stringify({ name, job }) }) .then(() => { console.log('add succuess') setLoading(false) }) .catch((error) => { setLoading(false) setError(error.message) }) }
|
接著元件的部分再改寫成這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import './styles.css' import { useForm } from './useForm'
export default function App() { const { name, job, updateJob, updateName, addNewUser, error, loading } = useForm()
const handleSubmit = (e) => { addNewUser(e).then(() => console.log('redirect to some where.')) }
return ( <div className='container'> <h1>Catch Error at custome hook</h1> <form onSubmit={handleSubmit}> {error && <div className='error'>{error}</div>} <div className='label'>Name: </div> <input type='text' value={name} onChange={updateName} /> <div className='label'>Job: </div> <input type='text' value={job} onChange={updateJob} /> <div className='btn-wrap'> <button>{loading ? 'sending...' : 'submit'}</button> </div> </form> </div> ) }
|
這樣就完成了。
OK,剛剛說的都只是鋪陳,現在要講的東西才是重頭戲:
如果我也想要在外面 catch 錯誤的話怎麼辦?
這個就是一開始標題提到的情境,希望做到這邊後你有清楚一些了。
之所以要這樣是因為我們不想要在發生錯誤時做 redirect 的動作,所以會希望出錯時能夠 catch 錯誤。
這時候你可能會問說「阿就跟剛剛一樣把 catch 挪到最外層來做不就好了?」,像是這樣:
1 2 3 4 5 6 7 8 9 10 11 12
| const addNewUser = async (e) => { e.preventDefault() setLoading(true) await fetch(url, { method: 'post', body: JSON.stringify({ name, job }) }).then(() => { console.log('add succuess') setLoading(false) }) }
|
但很抱歉,不行。
當你這樣做以後,你會發現另一個問題:
- 沒辦法取消 loading 狀態
- 沒辦法把表單的錯誤訊息秀出來
不要忘了這兩個 state 都是封裝在裡面來處理的,所以除非你把 hook 裡的 setError
跟 setLoading
也 export 到外面給元件用,不然你沒辦法解決這個問題。
那該怎麼辦才好呢?
- 永遠不要忘了我們可以自己拋出 Error
- 永遠不要忘了我們可以自己拋出 Error
- 永遠不要忘了我們可以自己拋出 Error
雖然聽起來很奇妙,但這個就是解法,只要在 catch 中在拋出另一個 Error 就好了,像這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const addNewUser = async (e) => { e.preventDefault() setLoading(true) await fetch(url, { method: 'post', body: JSON.stringify({ name, job }) }) .then(() => { console.log('add succuess') setLoading(false) }) .catch((error) => { setLoading(false) setError(error.message) throw new Error(err) }) }
|
因為這個 Error 是在 catch
中發生的,所以它就會到外面找另一個 catch
來接住。
透過這種手法就可以讓元件也能 catch
錯誤,然後做對應的處理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import './styles.css' import { useForm } from './useForm'
export default function App() { const { name, job, updateJob, updateName, addNewUser, error, loading } = useForm()
const handleSubmit = (e) => { addNewUser(e) .then(() => console.log('redirect to some where.')) .catch((error) => console.log(error.message)) }
return ( <div className='container'> <h1>Catch Error at custome hook</h1> <form onSubmit={handleSubmit}> {error && <div className='error'>{error}</div>} <div className='label'>Name: </div> <input type='text' value={name} onChange={updateName} /> <div className='label'>Job: </div> <input type='text' value={job} onChange={updateJob} /> <div className='btn-wrap'> <button>{loading ? 'sending...' : 'submit'}</button> </div> </form> </div> ) }
|
好啦,這個就是這篇要講的東西,有任何疑問的話可以到這邊來看範例。