React + TypeScript: Apollo Clientで操作エラーを扱う

[Apollo Client](https://www.apollographql.com/docs/react/)はReactで使える状態管理ライブラリです。ローカルとリモートのデータをGraphQLで扱えます。本稿は公式サイトの「[Handling operation errors](https://www.apollographql.com/docs/react/data/error-handling)」にもとづく、操作エラーをどう扱ったらよいかについての解説です。Apollo Clientでクエリを使うための基礎はすでに学んだことが前提となります(まだの方は先に「[React + TypeScript: Apollo ClientのGraphQLクエリを使ってみる](https://qiita.com/FumioNonaka/items/0c6b711627e3443ff73b)」をお読みください)。ドキュメントの邦訳ではなく、日本語で説明し直しました。原文から省いた部分もあり、逆にわかりにくいところは補っています。

Apollo ClientがGraphQLサーバーで操作を実行するとき、さまざまなエラーに遭遇するかもしれません。Apollo Clientは、エラーの種類に応じて正しく扱えるよう、エラーが起こったら適切な情報を示します。


# エラーの種類
リモートサーバーでGraphQLの操作を行ったとき、起こるかもしれないエラーはつぎのふたつです。

* GraphQLエラー
* ネットワークエラー

## GraphQLエラー
GraphQL操作のサーバー側の実行に関わります。つぎのようなエラーです。

* **構文エラー**: クエリの形式が正しくない場合など。
* **検証エラー**: クエリに存在しないスキーマフィールドが含まれていた場合など。
* **リゾルバエラー**: クエリフィールドをつくろうとしてエラーが発生した場合など。

構文エラーか検証エラーが起きた場合、サーバーは操作をまったく行いません。操作が無効だからです。リゾルバエラーが生じたときは、サーバーは一部のデータを返すこともあります。GraphQLのエラーについて、詳しくは「[Error handling](https://www.apollographql.com/docs/apollo-server/data/errors/)」をご参照ください。

GraphQLエラーが発生した場合、サーバーはApollo Clientへのレスポンスの`errors`配列にそのエラーを含めます。つぎのコードはエラーレスポンスの例です。

```json
{
	"errors": [
		{
			"message": "Cannot query field \"nonexistentField\" on type \"Query\".",
			"locations": [
				{
					"line": 2,
					"column": 3
				}
			],
			"extensions": {
				"code": "GRAPHQL_VALIDATION_FAILED",
				"exception": {
					"stacktrace": [
						"GraphQLError: Cannot query field \"nonexistentField\" on type \"Query\".",
						"...additional lines..."
					]
				}
			}
		}
	],
	"data": null
}
```

Apollo Clientは、これらのエラーを`useQuery`呼び出し(または用いた操作フック)が返した[`error.graphQLErrors`](https://www.apollographql.com/docs/react/data/queries/#result)配列に加えます。

GraphQLエラーによりApollo Serverが操作をまったくできない場合、`4xx`ステータスコードで応答します。リゾルバエラーが生じたときは、レスポンスに一部でもデータを含んでいれば、応答のステータスコードは`200`です。

### リゾルバ エラーのある部分データ
リゾルバエラーを起こした操作でも、一部のデータが返されることもあります。操作の要求したデータすべてではなく一部がサーバーのレスポンスに含まれているということです。Apollo Clientはデフォルトでは部分データは無視します。この動作は後述の「GraphQLエラーポリシー」を設定することによりオーバーライド可能です。

## ネットワークエラー
ネットワークエラーは、GraphQLサーバーと接続しようとしたときに起こります。通常、応答ステータスコードは、`4xx`または`5xx`です(データはなし)。

ネットワークエラーが生じると、Apollo Clientはそれを`useQuery`呼び出し(または用いた操作フック)が返した[`error.networkError`](https://www.apollographql.com/docs/react/data/queries/#result)フィールドに加えます。

再試行ロジックやその他の高度なネットワークエラー処理は、後述Apollo Linkを用いてアプリケーションに加えてください。


# GraphQLエラーポリシー
GraphQL操作で1つ以上のリゾルバエラーが生じた場合、サーバーのレスポンスは`data`フィールドに一部のデータを含んでいるかもしれません。

```json
{
	"data": {
		"getInt": 12,
		"getString": null
	},
	"errors": [
		{
			"message": "Failed to get string!"
			// ...その他のフィールド...
		}
	]
}
```

デフォルトでは、Apollo Clientは部分的なデータは破棄し、`useQuery`呼び出し(または用いたフック)の結果に[`error.graphQLErrors`](https://www.apollographql.com/docs/react/data/queries/#result)配列がつくられます。部分的な結果を使うために定めるのが、操作の**エラーポリシー**です。

Apollo Clientは、つぎのような操作のエラーポリシーをサポートします。

| ポリシー | 説明 |
| --- | --- |
| `none` | レスポンスにGraphQLエラーが含まれていると、`error.graphQLErrors`として返される。レスポンスの`data`は、サーバーが`data`を返したとしても、`undefined`に定められる。結果として、ネットワークエラーとGraphQLエラーの結果がほぼ同じかたちの応答になる。デフォルトのエラーポリシー。 |
| `ignore` | `graphQLErrors`は無視される(`graphQLErrors`がつくられない)。返された`data`はキャッシュされて、エラーが生じなかったかのようにレンダリングされる。 |
| `all` | `data`と`error.graphQLErrors`がともにつくられる。部分データとエラー情報のどちらもレンダリングできる。 |

## エラーポリシーを設定する
エラーポリシーは、つぎのように操作フック(`useQuery`など)に渡すオプションオブジェクトで定めます。

```react
const MY_QUERY = gql`
	query WillFail {
		badField # このフィールドのリゾルバはエラーを起こした
		goodField # このフィールドは正しくつくられた
	}
`;
function ShowingSomeErrors() {
	const { loading, error, data } = useQuery(MY_QUERY, { errorPolicy: 'all' });
	if (loading) return <span>loading...</span>;
	return (
		<div>
			<h2>Good: {data.goodField}</h2>
			<pre>
				Bad:{' '}
				{error.graphQLErrors.map(({ message }, i) => (
					<span key={i}>{message}</span>
				))}
			</pre>
		</div>
	);
}
```

このコード例で用いたエラーポリシーは`all`です。部分データとエラー情報の両方を、可能であればレンダリングします。


# Apollo Link による高度なエラー処理
Apollo Linkライブラリで設定できるのが、操作の実行中に発生したエラーの高度な処理です。

おすすめする最初のステップとして、[`onError`リンク](https://www.apollographql.com/docs/react/api/link/apollo-link-error)をリンクチェーンに加えれば、エラーの詳細が受け取れ、それに応じて動作させれられます。

以下に示すコード例は、`ApolloClient`コンストラクタに、ふたつのリンクが含まれたリンクチェーンを渡しています。

* `onError`: `graphQLErrors`または`networkError`がサーバーのレスポンスにあるかどうか確かめる。エラーがあれば、その詳細はログに記録される。
* `HttpLink`: 各GraphQL操作をサーバーに送信する。
    - チェーンの終端リンク。

```react
import { ApolloClient, HttpLink, InMemoryCache, from } from '@apollo/client';
import { onError } from '@apollo/client/link/error';

const errorLink = onError(({ graphQLErrors, networkError }) => {
	if (graphQLErrors)
		graphQLErrors.forEach(({ message, locations, path }) =>
			console.log(
				`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
			)
		);
	if (networkError) console.log(`[Network error]: ${networkError}`);
});
const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' })
const client = new ApolloClient({
	cache: new InMemoryCache(),
	link: from([errorLink, httpLink]),
});
```

## 操作の再試行
Apollo Linkは、失敗した操作を再試行することで、解決できる可能性がある場合にも役立ちます。発生するエラーの種類に応じて、異なるリンクをお使いください。

* `onError`はGraphQLエラー。
* `RetryLink`はネットワークエラー。

### GraphQLエラーの場合
`onError`リンクは、返されたGraphQLエラーの種類にもとづいて、失敗した操作を再試行できます。たとえば、トークンベースの認証を使っている場合、トークンの有効期限が切れたときに自動的に再認証を処理することです。

操作を再試行するには、`onError`関数の中から`forward(operation)`を返してください。コード例はつぎのとおりです。

```react
onError(({ graphQLErrors, networkError, operation, forward }) => {
	if (graphQLErrors) {
		for (let err of graphQLErrors) {
			switch (err.extensions.code) {
				// AuthenticationErrorリゾルバにスローされると
				// Apollo ServerはcodeにUNAUTHENTICATEDを設定
				case 'UNAUTHENTICATED':
					// Modify the operation context with a new token
					const oldHeaders = operation.getContext().headers;
					operation.setContext({
						headers: {
							...oldHeaders,
							authorization: getNewToken(),
						},
					});
					// リクエストを再試行してオブザーバブルが新たに返される
					return forward(operation);
			}
		}
	}
	// ネットワークエラーの再試行にはonErrorリンクでなくRetryLinkが推奨
	// ここではエラーのログのみ行う
	if (networkError) {
		console.log(`[Network error]: ${networkError}`);
	}
});
```

[注記] 再試行した操作でさらにエラーが生じた場合、`onError`リンクには渡されず、操作の無限ループは起こりません。つまり、`onError`リンクは特定の操作を1度だけ再試行できるのです。

操作を再試行したくない場合は、`onError`リンクの関数から何も返さないでください。

### ネットワークエラーの場合
ネットワークエラーになった操作の再試行は、`RetryLink`をリンクチェーンに加えるのがよいでしょう。このリンクを使えば、[指数バックオフ](https://yoshidashingo.hatenablog.com/entry/2014/08/17/135017)([exponential backoff](http://en.wikipedia.org/wiki/Exponential_backoff))や試行回数などの再試行ロジックが設定できます。詳しくは「[Retry Link](https://www.apollographql.com/docs/react/api/link/apollo-link-retry/)」をお読みください。

## `onError`リンクオプション
「Error Link」の「[Options](https://www.apollographql.com/docs/react/api/link/apollo-link-error/#options)」をご参照ください。