💡

【JS / TS】非同期通信(axios)ごとにtry~catchを実装したくない人のためのエラーハンドリング Tips💡

2022/03/24に公開
1

投稿する背景

初めての投稿となりますので、お手柔らかにご一読していただけると何よりです🙌

なぜ、今回重い腰を上げて本記事を執筆することにしたかと言うと、ある時、非同期通信処理(axios)を挟むごとに無意識でtry~catchを実装していた自分に疑問を抱いたからです。🤔
(進撃の巨人で言うと、「無意識に壁の外に人類が存在しない」という当たり前のように思っていたことから、真実に差し迫ったエルビン・スミスみたいな感じです。アニメ派なので執筆時点では最終回はまだ見ていません🙈)

try~catchを何度も実装せずにエラーをハンドリングできるインターフェースを持った関数を実装したら、どれだけ楽になるか、そう模索した結果、辿り着いた実装を共有したいと思います👀

対象読者

  • 非同期処理ごとに「try~catch〜」を書かなければいけない習慣に疲れた人😩
  • try~catchにおけるエラーハンドリングをもっと簡潔に実装したい人👀

読了後のゴール設定

  • 「try~catch〜」を1箇所の関数にまとめて、importして共通化して使用できる状態💡
  • 非同期通信の返り値をジェネリクスを用いて動的に指定できる状態💡

結論

globalUtilityType.ts
type SuccessResDataObjectType<T> = {
  data: T
  error: null
}

type FailedResErrorObjectType = {
  data: null
  error: Error
}
customAxiosRequest.ts
import axios from 'axios'
import type { FailedResErrorObjectType, SuccessResDataObjectType } from './globalUtilityType'

/**
 * @param httpRequest string
 * @returns Promise<{ data: T, error: null } | { data: null, error: Error }>
 */

class CustomAxiosRequest {
  public static pagesRequest = async <T>(httpRequest: string): Promise<SuccessResDataObjectType<T> | FailedResErrorObjectType> => {
    try {
      const { data } = await axios.get<T>(httpRequest)
      return {
        data,
        error: null,
      }
    } catch (error) {
      console.error(error)
      if (axios.isAxiosError(error)) {
        return {
          data: null,
          error,
        }
      }
      throw error
    }
  }
}

export default CustomAxiosRequest

使用方法例

index.ts
import CustomAxiosRequest from './customAxiosRequest'
import type { FailedResErrorObjectType, SuccessResDataObjectType } from './globalUtilityType'

export interface PostType {
    userId: number;
    id:     number;
    title:  string;
    body:   string;
}

const fetchData = async () => {
  const { data, error } = await CustomAxiosRequest.pagesRequest<[PostType]>("https://jsonplaceholder.typicode.com/posts")
  
  // NOTE: 成功していれば、dataがTruthyな値になる。
  if(data) {
    console.log("dataの取得に成功")
  }
  
  // NOTE: 失敗していれば、errorがTruthyな値になる。
    if(error) {
    console.log("dataの取得に失敗")
    throw error
  }
} 

サンドボックスで確認

解説

  1. axiosの型定義
index.ts
+ const { data } = await axios.get<T>(httpRequest)

axiosではジェネリクスを用いることで、レスポンス値のdataプロパティの値の型を動的に定義することができます。
つまり、dataの型は<T>で定義した型が当てはまることになる。

  1. 非同期通信の結果の型定義
globalUtilityType.ts
+ type SuccessResDataObjectType<T> = {
+   data: T
+   error: null
+ }
	
+ type FailedResErrorObjectType = {
+   data: null;
+   error: Error;
+ };

ここでも同じく型定義を使用するときにジェネリクスを使用しております。

つまり、CustomAxiosRequestクラスにおけるpagesRequestメソッドの返り値の型は、成功時は{ data: T error: null }となり、失敗時には、{ data: null error: Error }のようになる。

  1. 本題のpagesRequestメソッド
index.ts
+ public static pagesRequest = async <T>(httpRequest: string): Promise<SuccessResDataObjectType<T> | FailedResErrorObjectType> => { ... }

最後に、リクエストを出すpagesRequestメソッドについてです。
ジェネリクスが連鎖して、少々ややこしく見える気はしますが、中身を紐解けば単純です。

async <T>(httpRequest: string)の<T>では、pagesRequestメソッドを呼び出すときに、開発者が<T>の型定義を決定することができます。

例えば、CustomAxiosRequest.pagesRequest<string>(url)と呼び出すこともできますし、上で上がっているようなCustomAxiosRequest.pagesRequest<[PostType]>(url)とすることができます。

1,2,3の併せて考えると、前者の場合は、axiosのレスポンス値(データ)の型が、{ data: string error: null }となり、後者の場合は{ data: [PostType] error: null }となる。

最後に、pagesRequestメソッドを使用して、返り値を分割代入を用いて、dataerrorを取り出して、if分を用いてdataがTruthyか否か、errorがTruthyか否かを判定するロジックを組み込んだら、いちいちtry~catchを用いずとも、非同期通信のハンドリング実装を行うことができます💡

最後に

いかがだったでしょうか??🤔

Zenn初投稿で、大変緊張してしまいましたが、このTipsがtry~cathchをもう書きたくない人の人助けになれば幸いです🙏

また、この実装が実務的に良くない点などがございましたら、コメントいただけたら嬉しいです👀
皆さんの多大な知見を吸収しながら、強強エンジニアへと1歩ずつステップアップしていけたらと思っています🙇‍♂️

文章力に自信がないのですが、今後もZennを通じてアウトプットをしていき、そして、エンジニアリングを楽しみ、成長していけたらと思います💨

Discussion

nap5nap5

neverthrowから提供されているResult型などを使って実装にチャレンジしてみました。

本記事で達成したい"try~catchを実装したくない"というコンセプトはBFF以外で達成できたのではないかと思います。

デモコードです。

https://codesandbox.io/p/sandbox/hopeful-snyder-z0sjg1?file=%2Fsrc%2Ffeatures%2Fping%2Frepository%2Findex.ts&selection=[{"endColumn"%3A2%2C"endLineNumber"%3A24%2C"startColumn"%3A1%2C"startLineNumber"%3A9}]

デモコードの抜粋です。

レポジトリ側

async function requestToBFF(): Promise<ServerSideEnvData> {
  const response: AxiosResponse<ServerSideEnvData, ErrorData> = await axios.get(
    '/api/ping'
  )
  const { data } = response
  return data
}

export class ServerSideEnvRepository implements ServerSideEnvFactory {
  async ping(): Promise<Result<ServerSideEnvData, ErrorData>> {
    return ResultAsync.fromPromise<ServerSideEnvData, ErrorData>(
      requestToBFF(),
      (e) => e as ErrorData
    ).map((value) => value)
  }
}

フック側

const usePingHook = () => {
  // https://stackoverflow.com/a/63113066/15972569
  const { data, error, refetch } = useQuery<ServerSideEnvData, ErrorData>(
    [SERVER_SIDE_ENV_KEY],
    async () => {
      const result = await pingRepository.ping()
      if (result.isErr()) {
        return Promise.reject(result.error)
      }
      return result.value
    },
    {
      onSuccess: function (data) {},
      onError: function (error) {},
      onSettled: function (data, error) {},
    }
  )
  return { data, error, refetch }
}

export default usePingHook

コンポーネント側

const Ping = () => {
  const { data, error, refetch } = usePingHook()

  if (error) {
    return (
      <PingLayout>
        <button
          onClick={() => {
            queryClient.removeQueries([SERVER_SIDE_ENV_KEY])
            refetch()
          }}
        >
          Refetch
        </button>
        <ShowMe data={error} />
      </PingLayout>
    )
  }

  if (!data) {
    return (
      <PingLayout>
        <p>{`loading...`}</p>
      </PingLayout>
    )
  }

  return (
    <PingLayout>
      <button
        onClick={() => {
          queryClient.removeQueries([SERVER_SIDE_ENV_KEY])
          refetch()
        }}
      >
        Latest Refresh
      </button>
      <ShowMe data={data} />
    </PingLayout>
  )
}

export default Ping

簡単ですが、以上です。