【GraphQL】エラーレスポンスの設計戦略

はじめに

こんにちは、PlexJob開発チームの栃川です。

最近、チーム内でGraphQLのエラーレスポンスについて議論する機会がありました。

そこで今回は、GraphQLにおけるエラーレスポンスの考え方や設計戦略について、改めて整理してみたいと思います。

想定している読者

  • GraphQLを触ったことがある方
  • GraphQLのエラーレスポンス設計について整理したい方

GraphQLのエラー

まず、GraphQLのエラーにはどのようなものがあるのでしょうか?

GraphQLの仕様書を確認したところ、下記いずれかのエラーに分類されるようです。

No エラーケース ステータスコード
1 JSON解析エラー 400 (Bad Request)
2 無効なパラメータ 400 (Bad Request)
3 クエリの解析エラー 200 (OK)
4 クエリの検証エラー 200 (OK)
5 オペレーション失敗 200 (OK)
6 変数の型エラー 200 (OK)
7 実行中のフィールドエラー 200 (OK)

(※ 上記はapplication/jsonの場合であり、application/graphql-response+jsonでは取り扱いに違いがある点に注意。くわしくはこちら

これらのエラーは下記2つに分類されます。

Request Errors

  • 表のID 1~6 に該当
  • GraphQLの文法エラーやリクエストパラメータの問題により、クエリの実行前 に発生するエラー
  • 主にクライアントのリクエストミスが原因

Field Errors

  • 表のID 7 に該当
  • 特定のフィールドの実行中 に発生するエラーで、値の解決 (resolve) や結果値の出力に失敗した場合などに起こる
  • GraphQLサーバー側の不具合が原因で発生することが多い
  • フィールドエラーが発生しても処理は継続され、部分的な結果が返される

参考: GraphQL Spec, GraphQL Serverとエラー処理

このように、一口に「エラー」といっても、それぞれに異なる特性があります。

後述するエラーレスポンスの設計においては、 Request ErrorsとField Errorsによる分類が直接の意思決定に関わるわけではありません。 しかし、これらの分類を理解しておくことで、エラーレスポンスを整理しやすくなり、適切な対応を検討しやすくなるため、今回紹介させていただきました。

エラーレスポンス設計の選択肢

では、前項で見たエラーをどのようにしてクライアントに伝えればよいのでしょうか?

GraphQLのエラー設計には、大きく以下の3つのアプローチがあります。

  • 標準仕様に従う
  • Errors as data
  • Union / Result Types

それぞれの方法にはメリットとデメリットがあり、ユースケースやプロジェクトの要件によって適切な選択が求められます。 以降の節では、これら3つの方法について具体的に紹介していきます。

標準仕様に従う

GraphQLの標準仕様では、いかなるエラーであってもerrorsフィールドにメッセージを格納して、クライアントに送信するようになっています。

GraphQLのエラーは本来、例外的な事象やクライアント関連の問題を表すために設計されており、開発者向けのエラーメッセージを伝えるものとされています。そのため、必ずしもエンドユーザーに伝えるべきプロダクトやビジネス上のエラーが errorsフィールドに含まれるとは限りません。

GraphQL errors were originally designed to represent exceptional events and client-related issues, not necessarily expected product or business errors that need to be relayed to the end-user.

引用: Production Ready GraphQL

例えば、以下のような形でエラーレスポンスが返却されます。

{
  "errors": [
    {
      "message": "System is not available.",
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR"
      }
    }
  ],
  "data": {
    "user": {
      "name": "plexjob",
    }
  }
}

参考: GraphQL Spec, 公式ドキュメント

メリットは、標準仕様であるがゆえに、多くのGraphQLクライアントライブラリでサポートされており、手軽に実装できます。

デメリットは、クライアント側でどのようなエラーが返ってくるのかわかりにくくなる可能性があるという点です。 冒頭で紹介したRequest ErrorやField Errorなどの分類に関係なく、すべてのエラー内容をerrorsフィールドに含めてしまうため クライアント側では、どのようなエラーが返ってくるのかわかりにくくなります。

GraphQL Specの作者も、GraphQLのエラーレスポンスは「システムエラー(※1)」を扱うべきで、エンドユーザーによって引き起こされるような「業務エラー(※1)」はデータとして返すべきと述べています。

The general philosophy at play is that Errors are considered exceptional. Your user data should never be represented as an Error. If your users can do something that needs to provide negative guidance, then you should represent that kind of information in GraphQL as Data not as an Error. Errors should always represent either developer errors or exceptional circumstances (e.g. the database was offline).

引用: Validations that cannot be ran on the client side and the errors object · Issue #117 · graphql/graphql-spec · GitHub

また、後述する2つの手法とは異なり、スキーマ外でエラーを管理するため、宣言的な定義ができない点も、エラーの返却内容が予測しにくくなる原因となっています。

※1: システムエラーと業務エラーについて

Errors as data

Errors as dataは、「エラーもデータとしてクライアント側に示すべきだ」という考えのもと、ペイロード型(Payload Type)にエラーを示すフィールドを追加してエラーメッセージを表現します。

例えば、以下のようなスキーマ例では、SignUpPayload に userErrors フィールドが定義されており、エラーの詳細を格納できるようになっています。

type SignUpPayload {
  userErrors: [UserError!]!
  account: Account
}

type UserError {
  # The error message
  message: String!

  # Indicates which field cause the error, if any
  #
  # Field is an array that acts as a path to the error
  # Example:
  #
  # ["accounts", "1", "email"]
  #
  field: [String!]

  # An optional error code for clients to match on.
  code: UserErrorCode
}

Shopify GraphQL Design Tutorial においても同様の手法が取られていました。

メリットとしては、スキーマで管理できるため、クライアント側がどんなエラーメッセージを受け取るのか予測しやすいという点があります。また、独自のエラーレスポンスのデータ構造を定義できるため、必要な情報を柔軟に設計できます。さらに、userErrors を標準化することで、クライアント側は常に同じ構造でエラー処理を実装できるため、コードの可読性や保守性が向上します。

デメリットとしては、クライアントが userErrors フィールドを明示的にクエリしなければ、エラー情報を取得できないことです。その結果、意図せず null のデータだけを受け取り、なぜ失敗したのか分からない状況に陥る可能性があります。つまり、エラーを正しく処理するために、開発者が常に userErrors を意識する必要があるため、認知負荷が高まるという課題があります。

Union / Result Types

Union / Result Typesは、エラー専用のフィールドを設ける代わりに、Union型を利用して成功時とエラー時の異なる結果を表現する方法です。

例えば、以下のようなスキーマでは、SignUpPayload を SignUpSuccess、UserNameTaken、PasswordTooWeak という複数の型で構成しています。

type Mutation {
  signUp(email: String!, password: String!): SignUpPayload
}

union SignUpPayload =
  SignUpSuccess |
  UserNameTaken |
  PasswordTooWeak

type SignUpSuccess {
  account: Account
}

type UserNameTaken {
  message: String!
  suggestedUsername: String
}

type PasswordTooWeak {
  message: String!
  passwordRules: [String!]!
}

クライアント側では、次のように結果ごとに適切な処理を実装できます。

mutation {
  signUp(email: "marc@example.com", password: "P@ssword") {
    ... on SignUpSuccess {
      account {
        id
      }
    }
    ... on UserNameTaken {
      message
      suggestedUsername
    }
    ... on PasswordTooWeak {
      message
      passwordRules
    }
  }
}

メリットとしては、Union型を使用することで、成功パターンとエラーパターンを型として明確に分離できます。その結果、クライアント側はそれぞれのケースに対して適切な処理を実装しやすくなります。また、各エラーシナリオにカスタムフィールド(例: suggestedUsername や passwordRules)を追加できるため、エラーの表現力が豊富です。

デメリットとしては、クライアントがすべての可能な型に対して明示的にフラグメントを定義しなければ、エラー情報を取得できないことです。また、エラーごとに個別の型を定義する必要があるため、スキーマが肥大化しやすくクエリも煩雑化しやすいという課題もあります。

どの設計方針を選ぶべきか

ここまでの内容を踏まえて、各エラーレスポンスの設計手法のメリデリを整理します。

手法 メリット デメリット
標準仕様に従う ・手軽に実装できる ・どのようなエラーが返ってくるか予測しにくい
スキーマ外で管理される
Errors as Data ・独自のエラーデータ構造を定義できる ・クライアント側での認知負荷が高い
Union / Result Types ・成功とエラーを型として明確に分離できる(強い型付け) スキーマの肥大化・クエリの煩雑化

では一体、どのような設計戦略をとればいいのでしょうか?

Production Ready GraphQL』では下記のような記述がありました。

Honestly, as long as “user errors” are well defined in the schema, I don’t have a strong opinion on how they should be implemented.

一概に「これが正解」という明確な答えはないと述べつつも、 この言葉からは、「システムエラー」と「業務エラー」を明確に分離し、特に「業務エラー」はスキーマで管理された形で実装すべきだという強いメッセージが感じられます。

つまり、重要なのは「業務エラーをスキーマ上で明確に定義し、クライアントが正しく扱える状態にすること」である、という考え方が根底にあるようです。

設計戦略に関する考察

ここからは私個人の意見です。

私としては、現実としてチーム全員がGraphQLに精通しているとは限らず、新しいメンバーがジョインした際の学習コストやオンボーディングの負担を考慮すると、標準仕様に沿ったシンプルな実装が有効な場合もあると考えています。

また、アプリケーションの性質によっては、複雑なエラーハンドリングが不要なケースも少なくありません。たとえば、内部向けのツールや小規模なAPI、あるいは短期間での開発が求められるプロジェクトでは、実装の手軽さや保守のしやすさが優先されることもあります。

さらに、既存のGraphQLクライアントライブラリやツール群が標準仕様に最適化されていることも多く、追加のカスタマイズが不要な点は大きな利点です。こうした観点から、標準仕様に沿ったシンプルなエラーハンドリングも十分に実用的な選択肢であると言えるのではないかと思います。

実際、「標準仕様に従う」という意思決定(あるいは検討)をしている企業様も見受けられました。私が所属するチームにおいても、この手法を採用した実績があります。

techblog.zozo.com

zenn.dev

結局のところ、「これが正解」という唯一の答えは存在しません。 最終的に、「どの手法を採用するか」ではなく、「その選択がチームとプロジェクトにとってどのような価値をもたらすか」を基準に判断することが、納得感のある意思決定につながるのではないかと考えています。

まとめ

今回は、GraphQLのエラーレスポンスについて自分なりに整理してみました。

どの手法にもメリット・デメリットがあり、プロジェクトの規模やチームの状況に応じて最適な方法を選ぶことが重要だと感じました。

変化の激しい開発現場で、より良い意思決定をするためにも、各手法の特徴を理解し、整理しておくことが大きな助けになるはずです。

このブログが、皆さんの開発に少しでも役立てば幸いです!

参考文献

主に下記資料を参考にさせていただきました。 ありがとうございます!

Production Ready GraphQL

GraphQL Serverとエラー処理

GraphQL Spec

最後に

現在プレックスでは、ソフトウェアエンジニア、フロントエンドエンジニア、UIデザイナーを募集しています。

少しでも興味を持っていただけた方は、カジュアル面談だけでも大歓迎です!

ぜひお気軽にご連絡ください! 🚀

dev.plex.co.jp