1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ApolloServer + ApolloClientのエラー通知

Last updated at Posted at 2021-12-22

はじめに

若干ニッチな内容ですが、ApolloServer + ApolloClient を使ったwebアプリケーションにおけるエラーの通知に関して考えてみます。

前提

  1. フロントエンドのアプリケーションは ApolloClient を使いバックエンドのGraphQLサーバーと通信、バックエンドは ApollServer をベースにGraphQLリクエストを受け付け、各resolverにて処理を実行した後に適切なレスポンスをフロントエンドに返却します。
  2. フロントエンド・バックエンドそれぞれに errorLogger というエラー通知をするために関数を用意し、チーム内で決めた手法でエラーログの送信を行います(Sentry、CloudWatchなど)。

バックエンド

以下のような形のエラー通知が考えられます。

  1. resolverごとにエラー通知を行う
  2. ApolloServerの didEncounterErrors イベントを活用する

1. resolverごとにエラー通知を行う

親にあたるresolverまで子/孫の関数のエラーを伝播させ errorLogger を呼ぶ、もしくはresolverから一つの子の関数が呼ばれる場合(ApplicationServiceレイヤーのメソッドなど)はそちらで errorLogger を呼ぶといった形です。

const resolvers = {
  Query: {
    user(parent, args, context, info) {
      // もしくはApplicationServiceレイヤーのメソッドを呼び、そのメソッド内でtry/catchするなど
      try {
        return users.find(user => user.id === args.id);
      } catch (e) {
        errorLogger(e);
      }
    }
  }
}

若干話がそれますが、エラーを通知後、レスポンスとして返すエラーを formatError 関数内で整形している場合、context(公式ドキュメントによると認証処理等を行う場所、という認識)からthrowされるエラーは ApolloError のインスタンスでないとランタイムエラーとなってしまう現象がありました(resolverからthrowされるエラーは GraphQLError というクラスのインスタンスに変換された上で formatError に渡っているように見えます)。

2. ApolloServerの didEncounterErrors イベントを活用する

Sentryの公式ブログ でも紹介されているのですが、ApolloServerの didEncounterErrors イベントを活用しエラーを送信する、というやり方もあります。ただしこの場合だとエラーの内容を元に1箇所で処理を分岐することになってしまうかと(この場合はこういったエラーを送信する、もしくはこの場合はエラーを送信しないなど)。

個人的には1で良いかと思っています。

フロントエンド

以下のような形のエラー通知が考えられます。

  1. GraphQLのquery/mutationそれぞれのoperationごとの error レスポンスデータを確認してエラー通知
  2. ApolloClientのインスタンス化の際に onError 関数を errorLink に渡す

1. GraphQLのquery/mutationそれぞれのoperationごとの error レスポンスデータを確認してエラー通知

const { loading, error, data } = useQuery(GET_DOGS);
if (error) {
  // 実際にはもう少しerrorの中身を確認してから通知するか決める
  errorLogger(error);
}

GraphQLのquery/mutationそれぞれのoperationごとの error レスポンスデータを確認し、必要な際に都度 errorLogger 関数を呼びます。通知だけでなく、特定のエラーが発生した際に共通の関数を呼び、エラーハンドリングを行う、といった処理もここでできそうです。

2. ApolloClientのインスタンス化の際にonError関数をerrorLinkに渡す

const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    // エラーの内容に応じて errorLoggerを呼ぶ
  }
}

const client = new ApolloClient({
  cache: new InMemoryCache()
  link: from([errorLink, httpLink]),
});

この onError 関数内で errorLogger を呼ぶ形なのですがいくつか懸念点があります。

  1. ApolloServerからのレスポンスの内容( code など)を元にonError関数内で処理を分岐することになり(この場合はこういったエラーを送信する、もしくはこの場合はエラーを送信しないなど)、分岐の処理がどんどん増えていく可能性がある
  2. プロジェクトでSentryを使っているとこのonError関数でエラーが上手くcatchされず、本来に握りつぶしたかったエラーがSentryに送信されてしまう、という現象が過去に発生しました(こちらが恐らく関連issue)

なので基本的に1の方法で良いかと思っています。そしてそもそもフロントエンドのチームがApolloServerまで管理している体制下でフロントエンドのGraphQLエラーとバックエンドのエラーの内容が重複する場合、フロントエンド側の通知は省いてしまっても良いかもしれません(ネットワークの問題でGraphQLサーバーと通信できなかった場合は除く。その際はバックエンド側から感知できないため)。

まとめ

最近このエラー通知まわりで少し苦労したため、全体的な整理をしてみました。エラーの種類が将来的に増えても容易に個別対応ができる設計が良さそうです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?