diff --git a/packages/react-async/src/index.d.ts b/packages/react-async/src/index.d.ts index 4d46575e..77d88524 100644 --- a/packages/react-async/src/index.d.ts +++ b/packages/react-async/src/index.d.ts @@ -234,4 +234,8 @@ type FetchRun = { run(): void } +export class FetchError extends Error { + response: Response +} + export default Async diff --git a/packages/react-async/src/index.js b/packages/react-async/src/index.js index 5c8aba24..e9409c85 100644 --- a/packages/react-async/src/index.js +++ b/packages/react-async/src/index.js @@ -1,6 +1,6 @@ import Async from "./Async" export { default as Async, createInstance } from "./Async" -export { default as useAsync, useFetch } from "./useAsync" +export { default as useAsync, useFetch, FetchError } from "./useAsync" export default Async export { statusTypes } from "./status" export { default as globalScope } from "./globalScope" diff --git a/packages/react-async/src/useAsync.js b/packages/react-async/src/useAsync.js index b2c14509..1490a6b8 100644 --- a/packages/react-async/src/useAsync.js +++ b/packages/react-async/src/useAsync.js @@ -154,8 +154,15 @@ const useAsync = (arg1, arg2) => { ) } +export class FetchError extends Error { + constructor(response) { + super(`${response.status} ${response.statusText}`) + this.response = response + } +} + const parseResponse = (accept, json) => res => { - if (!res.ok) return Promise.reject(res) + if (!res.ok) return Promise.reject(new FetchError(res)) if (typeof json === "boolean") return json ? res.json() : res return accept === "application/json" ? res.json() : res } diff --git a/packages/react-async/src/useAsync.spec.js b/packages/react-async/src/useAsync.spec.js index b6501578..77a4602f 100644 --- a/packages/react-async/src/useAsync.spec.js +++ b/packages/react-async/src/useAsync.spec.js @@ -3,7 +3,7 @@ import "@testing-library/jest-dom/extend-expect" import React from "react" import { render, fireEvent, cleanup } from "@testing-library/react" -import { useAsync, useFetch, globalScope } from "./index" +import { useAsync, useFetch, globalScope, FetchError } from "./index" import { sleep, resolveTo, @@ -20,11 +20,11 @@ const abortCtrl = { abort: jest.fn(), signal: "SIGNAL" } globalScope.AbortController = jest.fn(() => abortCtrl) const json = jest.fn(() => ({})) -globalScope.fetch = jest.fn(() => Promise.resolve({ ok: true, json })) +globalScope.fetch = jest.fn() beforeEach(abortCtrl.abort.mockClear) -beforeEach(globalScope.fetch.mockClear) beforeEach(json.mockClear) +beforeEach(() => globalScope.fetch.mockReset().mockResolvedValue({ ok: true, json })) afterEach(cleanup) const Async = ({ children = () => null, ...props }) => children(useAsync(props)) @@ -250,4 +250,20 @@ describe("useFetch", () => { expect.objectContaining({ preventDefault: expect.any(Function) }) ) }) + + test("throws a FetchError for failed requests", async () => { + const errorResponse = { ok: false, status: 400, statusText: "Bad Request", json } + globalScope.fetch.mockResolvedValue(errorResponse) + const onResolve = jest.fn() + const onReject = jest.fn() + render() + expect(globalScope.fetch).toHaveBeenCalled() + await sleep(10) + expect(onResolve).not.toHaveBeenCalled() + expect(onReject).toHaveBeenCalled() + let [err] = onReject.mock.calls[0] + expect(err).toBeInstanceOf(FetchError) + expect(err.message).toEqual("400 Bad Request") + expect(err.response).toBe(errorResponse) + }) })