はじめに
若干ニッチな内容ですが、ApolloServer
+ ApolloClient
を使ったwebアプリケーションにおけるエラーの通知に関して考えてみます。
前提
- フロントエンドのアプリケーションは ApolloClient を使いバックエンドのGraphQLサーバーと通信、バックエンドは ApollServer をベースにGraphQLリクエストを受け付け、各resolverにて処理を実行した後に適切なレスポンスをフロントエンドに返却します。
- フロントエンド・バックエンドそれぞれに
errorLogger
というエラー通知をするために関数を用意し、チーム内で決めた手法でエラーログの送信を行います(Sentry、CloudWatchなど)。
バックエンド
以下のような形のエラー通知が考えられます。
- resolverごとにエラー通知を行う
- 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で良いかと思っています。
フロントエンド
以下のような形のエラー通知が考えられます。
- GraphQLのquery/mutationそれぞれのoperationごとの
error
レスポンスデータを確認してエラー通知 - 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
を呼ぶ形なのですがいくつか懸念点があります。
- ApolloServerからのレスポンスの内容(
code
など)を元にonError関数内で処理を分岐することになり(この場合はこういったエラーを送信する、もしくはこの場合はエラーを送信しないなど)、分岐の処理がどんどん増えていく可能性がある - プロジェクトでSentryを使っているとこのonError関数でエラーが上手くcatchされず、本来に握りつぶしたかったエラーがSentryに送信されてしまう、という現象が過去に発生しました(こちらが恐らく関連issue)
なので基本的に1の方法で良いかと思っています。そしてそもそもフロントエンドのチームがApolloServerまで管理している体制下でフロントエンドのGraphQLエラーとバックエンドのエラーの内容が重複する場合、フロントエンド側の通知は省いてしまっても良いかもしれません(ネットワークの問題でGraphQLサーバーと通信できなかった場合は除く。その際はバックエンド側から感知できないため)。
まとめ
最近このエラー通知まわりで少し苦労したため、全体的な整理をしてみました。エラーの種類が将来的に増えても容易に個別対応ができる設計が良さそうです。