Repository: FormidableLabs/urql
Pull request #947 (2020-08-27 16:39:08)

(exchanges) implement error exchange
## Summary

An exchange that allows inspecting all errors within the urql client.

```js
import { errorExchange, dedupExchange, fetchExchange } from 'urql';

const client = new Client({
  exchanges: [
    dedupExchange,
    errorExchange({
      onError: (error: CombinedError, operation: Operation) => {
        console.log("An error!", error);
      }
    }),
    fetchExchange
  ],
});
```


## Set of changes

- adding an error exchange to urql core
- adding a note about the error exchange in the documentation site
- tests! 👏 


changeset-bot[bot]: ###  💥  No Changeset

Latest commit: 706d3beb3a3cb395166f6c8ca5bee808d4459e93

Merging this PR will not cause any packages to be released. If these changes should not cause updates to packages in this repo, this is fine 🙂

**If these changes should be published to npm, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/FormidableLabs/urql/new/feature/error-exchange?filename=.changeset/five-chefs-reply.md&value=---%0A%22%40urql%2Fcore%22%3A%20patch%0A---%0A%0A(exchanges)%20implement%20error%20exchange%0A)



Patch:
diff --git a/.changeset/many-comics-grin.md b/.changeset/many-comics-grin.md
new file mode 100644
index 00000000..43bd5afe
--- /dev/null
+++ b/.changeset/many-comics-grin.md
@@ -0,0 +1,5 @@
+---
+'@urql/core': minor
+---
+
+Adds an error exchange to urql-core. This allows tapping into all graphql errors within the urql client. Useful for logging, debugging, handling authentication errors etc
diff --git a/docs/api/core.md b/docs/api/core.md
index 6d4b8161..418b88ff 100644
--- a/docs/api/core.md
+++ b/docs/api/core.md
@@ -335,6 +335,20 @@ and is still waiting for a result.
 The `fetchExchange` of type `Exchange` is responsible for sending operations of type `'query'` and
 `'mutation'` to a GraphQL API using `fetch`.
 
+### errorExchange
+
+An exchange that lets you inspect errors. This can be useful for logging, or reacting to
+different types of errors (e.g. logging the user out in case of a permission error).
+
+```ts
+errorExchange({
+  onError: (error: CombinedError, operation: Operation) => {
+    console.log("An error!", error);
+  }
+})
+
+```
+
 ## Utilities
 
 ### stringifyVariables
diff --git a/packages/core/src/exchanges/error.ts b/packages/core/src/exchanges/error.ts
new file mode 100644
index 00000000..3127e105
--- /dev/null
+++ b/packages/core/src/exchanges/error.ts
@@ -0,0 +1,18 @@
+import { pipe, tap } from 'wonka';
+import { Exchange, Operation } from '../types';
+import { CombinedError } from '../utils';
+
+export const errorExchange = ({
+  onError,
+}: {
+  onError: (error: CombinedError, operation: Operation) => void;
+}): Exchange => ({ forward }) => ops$ => {
+  return pipe(
+    forward(ops$),
+    tap(({ error, operation }) => {
+      if (error) {
+        onError(error, operation);
+      }
+    })
+  );
+};
diff --git a/packages/core/src/exchanges/index.ts b/packages/core/src/exchanges/index.ts
index 798c4f48..ef7b6391 100644
--- a/packages/core/src/exchanges/index.ts
+++ b/packages/core/src/exchanges/index.ts
@@ -6,6 +6,7 @@ export { dedupExchange } from './dedup';
 export { fetchExchange } from './fetch';
 export { fallbackExchangeIO } from './fallback';
 export { composeExchanges } from './compose';
+export { errorExchange } from './error';
 
 import { cacheExchange } from './cache';
 import { dedupExchange } from './dedup';


Test patch:
diff --git a/packages/core/src/exchanges/error.test.ts b/packages/core/src/exchanges/error.test.ts
new file mode 100644
index 00000000..a7069120
--- /dev/null
+++ b/packages/core/src/exchanges/error.test.ts
@@ -0,0 +1,117 @@
+import { makeSubject, map, pipe, publish, Subject } from 'wonka';
+import { Client } from '../client';
+import { queryOperation } from '../test-utils';
+import { makeErrorResult, CombinedError } from '../utils';
+import { Operation } from '../types';
+import { errorExchange } from './error';
+
+const error = new Error('Sad times');
+let input: Subject<Operation>;
+
+beforeEach(() => {
+  input = makeSubject<Operation>();
+});
+
+it('does not trigger when there are no errors', async () => {
+  const onError = jest.fn();
+  const { source: ops$, next, complete } = input;
+  const exchangeArgs = {
+    forward: op$ =>
+      pipe(
+        op$,
+        map((operation: Operation) => ({ operation }))
+      ),
+    client: {} as Client,
+    dispatchDebug: () => null,
+  };
+  const exchange = errorExchange({ onError })(exchangeArgs)(ops$);
+
+  publish(exchange);
+  next(queryOperation);
+  complete();
+  expect(onError).toBeCalledTimes(0);
+});
+
+it('triggers correctly when the operations has an error', async () => {
+  const onError = jest.fn();
+  const { source: ops$, next, complete } = input;
+  const exchangeArgs = {
+    forward: op$ =>
+      pipe(
+        op$,
+        map((operation: Operation) => makeErrorResult(operation, error))
+      ),
+    client: {} as Client,
+    dispatchDebug: () => null,
+  };
+  const exchange = errorExchange({ onError })(exchangeArgs)(ops$);
+
+  publish(exchange);
+  next(queryOperation);
+  complete();
+  expect(onError).toBeCalledTimes(1);
+  expect(onError).toBeCalledWith(
+    new CombinedError({ networkError: error }),
+    queryOperation
+  );
+});
+
+it('triggers correctly multiple times the operations has an error', async () => {
+  const onError = jest.fn();
+  const { source: ops$, next, complete } = input;
+
+  const firstQuery = {
+    ...queryOperation,
+    context: {
+      ...queryOperation.context,
+      item: 1,
+    },
+  };
+
+  const secondQuery = {
+    ...queryOperation,
+    context: {
+      ...queryOperation.context,
+      item: 2,
+    },
+  };
+
+  const thirdQuery = {
+    ...queryOperation,
+    context: {
+      ...queryOperation.context,
+      item: 3,
+    },
+  };
+
+  const exchangeArgs = {
+    forward: op$ =>
+      pipe(
+        op$,
+        map((operation: Operation) => {
+          if (operation.context.item === 2) {
+            return { operation };
+          }
+          return makeErrorResult(operation, error);
+        })
+      ),
+    client: {} as Client,
+    dispatchDebug: () => null,
+  };
+  const exchange = errorExchange({ onError })(exchangeArgs)(ops$);
+
+  publish(exchange);
+  next(firstQuery);
+  next(secondQuery);
+  next(thirdQuery);
+  complete();
+  expect(onError).toBeCalledTimes(2);
+  expect(onError).toBeCalledWith(
+    new CombinedError({ networkError: error }),
+    firstQuery
+  );
+  expect(onError).toBeCalledWith(
+    new CombinedError({ networkError: error }),
+    thirdQuery
+  );
+});