點燈坊

失くすものさえない今が強くなるチャンスよ

使用 catchError() 與 EMPTY 處理 Asynchronous Error

Sam Xiao's Avatar 2020-07-04

由於 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

catcherror000

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

catcherror001

若在斷網情況下,不只沒有結果,還會提示 Uncaught 未處理 asynchronous error。

更糟糕的是僅管網路恢復,RxJS 也無法正常執行,因為 asynchronous error 已經造成 stream 中斷,這才是最麻煩之處。

catchError() vs. EMPTY

catcherror002

使用 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 狀態

catch005

也因為 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 不中斷

catcherror003

ab 為原本 Observable 遇到 x error,由 catchError() 提供新的 123 … Observable 繼續。

Error Message

catch005

若想 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.status0,可利用 || 視為 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

Reference

RxJS, catchError()
RxJS, EMPTY