もなかアイスの試食品

「とりあえずやってみたい」そんな気持ちが先走りすぎて挫折が多い私のメモ書きみたいなものです.

Javascriptで可読性と保守性を意識したエラー処理を考えてみた

はじめに

Webアプリを作成で、Httpのステータスコードや、Javascriptの例外によってユーザにわかりやすいメッセージを表示したい

このとき特に何も考えずにコーディングすると、try-catchのcatch句の中で、結構なif文(またはswitch文)が出てきそうな気がする

しかも条件判断はほとんど同じなんだけれども、細かいところが違うだけみたいな(404エラーのときは、ある変数に文言を代入する。「ある変数」だけが色んなところで微妙に違う)

ということで勉強も理解もしていないけれども、確かこんなデザインパターンあったよね?ということで、Javascriptのエラー処理を読みやすくする方法について考えてみた

とりあえず良く書きそうなエラー処理

try {
  // axiosでAPI呼び出し
} catch (e) {
  if (!e.response) {
    this.errorMessage = 'サーバに接続できません'
  } else {
    switch (e.response.status) {
      case 401:
        this.errorMessage = 'ログインID、またはパスワードが異なります'
        break
      case 500:
        this.errorMessage = '処理中に問題が発生しました'
        break

      // 中略

    }
  }
}

switch文のbreakって邪魔だよなぁ・・・

switch文をif文にしても、個人的には「ゴチャゴチャしてるなぁ」と感じる

ネストしたif文が結構嫌いなのかもしれない

try {
  // axiosでAPI呼び出し
} catch (e) {
  if (!e.response) {
    this.errorMessage = 'サーバに接続できません'
  } else {
    const status = e.response.status
    if (status === 401) {
      this.errorMessage = 'ログインID、またはパスワードが異なります'
    } else if (status === 500) {
      this.errorMessage = '処理中に問題が発生しました'
    }

    // 中略

    }
  }
}

switch文にしても、if文にしても「気を付けながら」コードを書いている気がする(しかも読むときも気を付けている)

あとAxiosでネットワークエラーが起きたかどうかを「!e.response」で判断しているけれども、バージョンアップで挙動が変わらないかちょっと心配

なんと言うか、ボケーッとしてても書けて、ボケーッとしても読めるように直感的な書き方にしたいんじゃ

メソッドチェーンや「チェイン・なんとかかんとか」というデザインパターンっぽいものを使えば読みやすくなると思ったので考えてみた

コードを考えてみた結果

とりあえず結果として、エラーハンドリングは以下のコードになった

import { ErrorHandler } from '../components/error-handler'

try {
  // axiosでAPI呼び出し
} catch (e) {
  ErrorHandler.builder(this, e)
    .unauthorized(function () {
      this.errorMessage = 'ログインID、またはパスワードが異なります'
    })
    .internalServerError(function () {
      this.errorMessage = '処理中に問題が発生しました'
    })
    .networkError(function () {
      this.errorMessage = 'サーバと通信が出来ません'
    })
    .handle()
}

ちなみにコレはVueコンポーネント内の処理の一部

エラーのコールバック関数の中で、「this」を使ってVueコンポーネントのdataのプロパティにアクセス出来るようになっている

以下がerror-handler.jsのソースコード

class AbstractHandler {
  constructor (thisArgs, chainContext, action) {
    this.thisArgs = thisArgs
    this.chainContext = chainContext
    if (typeof action !== 'function') {
      throw new TypeError('action is require function.')
    }
    this.action = action
  }

  invokeAction () {
    this.action.apply(this.thisArgs, this.chainContext.error)
    ++this.chainContext.handledCount
  }
}

class StatusCodeHandler extends AbstractHandler {
  constructor (thisArgs, chainContext, action, code) {
    super(thisArgs, chainContext, action)
    this.expectedCode = code
  }

  handle () {
    const e = this.chainContext.error
    if (!e.response) {
      return
    }
    if (e.response.status === this.expectedCode) {
      this.invokeAction()
    }
  }
}

class NetworkErrorHandler extends AbstractHandler {
  handle () {
    if (!this.chainContext.error.response) {
      this.invokeAction()
    }
  }
}

class ErrorHandlerBuilder {
  constructor (thisArgs, error) {
    this.thisArgs = thisArgs
    this.chainContext = {
      error: error,
      handledCount: 0
    }
    this.handlers = []
  }

  unauthorized (action) {
    this.handlers.push(new StatusCodeHandler(this.thisArgs, this.chainContext, action, 401))
    return this
  }

  internalServerError (action) {
    this.handlers.push(new StatusCodeHandler(this.thisArgs, this.chainContext, action, 500))
    return this
  }

  networkError (action) {
    this.handlers.push(new NetworkErrorHandler(this.thisArgs, this.chainContext, action))
    return this
  }

  handle () {
    this.handlers.forEach(handler => handler.handle())
    if (this.chainContext.handledCount === 0) {
      throw new Error('ErrorHandlerBuilder has unhandled error:\n' + this.chainContext.error)
    }
  }
}

export class ErrorHandler {
  static builder (thisArgs, error) {
    return new ErrorHandlerBuilder(thisArgs, error)
  }
}

おわりに

メソッドチェーンのおかげで、エラー処理の部分は自然な感じで読めるようになったと思う

あと統合開発環境ならピリオドを入力しただけでメソッドの候補が出てくるので、それだけでもエラー処理を書くのが楽になったのは地味に便利