由於 RxJS 的 Declarative 與 Stream 特性,因此在處理 Asynchronous Error 與處理 Rejected Promise 觀念不太一樣,重點在維持 Stream 不要死掉,讓下一個 Asynchronous Value 能正常發出。
Version
macOS Catalina 10.15.5
WebStorm 2020.1.2
Vue 2.6.11
RxJS 6.5.5
Browser
Asynchronous 若成功回傳,可正確顯示 title 為 FP in JavaScript
。
Data
{
"id": 1,
"title": "FP in JavaScript",
"price": 100,
"categoryId": 1,
"image": "fpjs.jpg"
}
http://localhost:3000/books/:id
會回傳指定 id
的書籍。
Asynchronous Success
<template>
<div>
<button v-stream:click="{ subject: click$, data: 1 }">Get Title</button>
{{ title$ }}
</div>
</template>
<script>
import { ajax } from 'rxjs/ajax'
import { concatMap, pluck } from 'rxjs/operators'
let fetchBook$ = x => ajax(`http://localhost:3000/books/${x}`).pipe(
pluck('response'),
)
let subscriptions = function() {
let title$ = this.click$.pipe(
pluck('data'),
concatMap(fetchBook$),
pluck('title'),
)
return { title$ }
}
export default {
name:'App',
domStreams: [
'click$'
],
subscriptions
}
</script>
12 行
let fetchBook$ = x => ajax(`http://localhost:3000/books/${x}`).pipe(
pluck('response'),
)
fetchBook$
使用 RxJS 的 ajax()
直接回傳 Observable。
17 行
let title$ = this.click$.pipe(
pluck('data'),
concatMap(fetchBook$),
pluck('title'),
)
使用 concatMap()
對 fetchBook$()
抓資料,由於只需顯示 title
,特別使用 pluck()
取出 title
property。
當 asynchronous 一切都成功時,這樣寫完全沒問題。
Asynchronous Error
若在斷網情況下,不只沒有結果,還會提示 Uncaught
未處理 asynchronous error。
更糟糕的是僅管網路恢復,RxJS 也無法正常執行,因為 asynchronous error 已經造成 stream 中斷,這才是最麻煩之處。
catchError() vs. EMPTY
使用 catchError()
與 EMPTY
處理 asynchronous error 後,就不再出現 Uncaught
錯誤,且網路恢復後 RxJS 也會繼續正常執行。
<template>
<div>
<button v-stream:click="{ subject: click$, data: 1 }">Get Title</button>
{{ title$ }}
</div>
</template>
<script>
import { EMPTY } from 'rxjs'
import { ajax } from 'rxjs/ajax'
import { concatMap, pluck, catchError } from 'rxjs/operators'
let fetchBook$ = x => ajax(`http://localhost:3000/books/${x}`).pipe(
pluck('response'),
catchError(_ => EMPTY)
)
let subscriptions = function() {
let title$ = this.click$.pipe(
pluck('data'),
concatMap(fetchBook$),
pluck('title'),
)
return { title$ }
}
export default {
name:'App',
domStreams: [
'click$'
],
subscriptions
}
</script>
13 行
let fetchBook$ = x => ajax(`http://localhost:3000/books/${x}`).pipe(
pluck('response'),
catchError(_ => EMPTY)
)
在 API function 內使用 catchError()
捕捉 asynchronous error,與一般 Imperative 的 catch
都是以 side effect 印出 error log 不同,由於還在 stream 中,RxJS 要求在 catchError()
內回傳 Observable 使 stream 不中斷,在此回傳 EMPTY
Observable。
EMPTY
回傳沒有任何內容的 Observable 並直接進入 complete 狀態
也因為 EMPTY
Observable 直接進入 complete 狀態,所以不會執行 pluck()
與 concatMap()
,因此不用擔心 pluck()
找不到 title
property 而出錯,是 RxJS 因為 error 快速結束執行的好方法,重點是還不會中斷 stream。
let title$ = this.click$.pipe(
pluck('data'),
exhaustMap(fetchBook$),
catchError(_ => EMPTY)
)
初學者常會將 catchError()
寫在 click$
的 pipe()
,這樣寫第一次斷網會顯示 FP in JavaScript
,也沒有 Uncaught
錯誤,但之後僅管網路恢復,RxJS 也無法在繼續執行了。
因為當 fetchBook$()
產生 asynchronous error 時,其 pipe()
內並沒有 catchError()
,因此將 error 往外層傳,click$
的 catchError()
收到 error 處理,但重點是 fetchBook$()
本身的 asynchronous error 並沒有處理, 造成 stream 中斷。
也就是在 RxJS 觀念中,唯有在該 Observable 內以 catchError()
處理,才能不中斷 stream,也就是 catchError()
必須寫在對的地方。
catchError()
處理 stream 中的 error,提供新的 Observable 使 stream 不中斷
a
、b
為原本 Observable 遇到 x
error,由 catchError()
提供新的 1
、2
、3
… Observable 繼續。
Error Message
若想 cateError()
不只回傳 EMPTY
外,還要順便能印出 error log 呢 ?
<template>
<div>
<button v-stream:click="{ subject: click$, data: 1 }">Get Title</button>
{{ title$ }}
</div>
</template>
<script>
import { EMPTY } from 'rxjs'
import { ajax } from 'rxjs/ajax'
import { catchError, concatMap, pluck } from 'rxjs/operators'
let fetchBook$ = x => ajax(`http://localhost:3000/books/${x}`).pipe(
pluck('response'),
catchError(e => (console.log(e.status || '404'), EMPTY))
)
let subscriptions = function() {
let title$ = this.click$.pipe(
pluck('data'),
concatMap(fetchBook$),
pluck('title')
)
return { title$ }
}
export default {
name:'App',
domStreams: [
'click$'
],
subscriptions
}
</script>
13 行
let fetchBook$ = x => ajax(`http://localhost:3000/books/${x}`).pipe(
pluck('response'),
catchError(e => (console.log(e.status || '404'), EMPTY))
)
由於想先用 console.log()
印出 error,再回傳 EMPTY
Observable,因此使用 comma operator 將 console.log()
與 EMPTY
合併成一行由左向右執行。
當斷網時 e.status
為 0
,可利用 ||
視為 falsy value 回傳 404
。
如此不僅不會中斷 stream,也可以完美以 side effect 印出 error log。
Conclusion
- FRP 在處理 error 與 Imperative 觀念不同,Imperative 是遇到 error 就直接處理 side effect,但 FRP 因為還在 stream 中,只能提供新 Observable 繼續使 stream 不中斷
- 記得
catchError()
要寫在 API Observable 的pipe()
內,而非 DOM event Observable 的pipe()
內,因為要處理的是 HTTP error,而非 DOM error