關於 Catch Error 的實際案例

錯誤處理很重要。

簡述

這篇可以當作是從 JavaScript-處理錯誤的方法 延伸出來的筆記,所以會假設你已經知道下面這幾個概念:

  1. 知道 try...catch..finally 的流程
  2. 知道 throw new Error() 是什麼?跟會發生什麼事?
  3. 知道 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 出去,然後進入 getInitDropdowncatch 區塊。

案例二:我想在 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 做的事情有幾個:

  1. 有 Error 時顯示錯誤訊息
  2. 在 Loading 時顯示讀取狀態
  3. 把 input 值跟 state 做綁定
  4. 表單送出時執行 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 才會等這邊執行完才 return
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()

// 記得要把 event 要傳進去才不會出錯
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)
})
// 把 catch 拿掉
}

但很抱歉,不行。

當你這樣做以後,你會發現另一個問題:

  1. 沒辦法取消 loading 狀態
  2. 沒辦法把表單的錯誤訊息秀出來

不要忘了這兩個 state 都是封裝在裡面來處理的,所以除非你把 hook 裡的 setErrorsetLoading 也 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)
// 改完 state 後拋出新的錯誤
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) => {
// 現在能 catch error 了
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>
)
}

好啦,這個就是這篇要講的東西,有任何疑問的話可以到這邊來看範例

關於 JWT(JSON-Web-Token) JavaScript-處理錯誤的方法
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×