Skip to content

Commit 428256a

Browse files
artdentghengeveld
authored andcommitted
Make sure useFetch rejects with an Error type. (#114)
* Make sure useFetch rejects with an Error type. Previously, a non-ok http response would reject with the response object. It's better for rejections to be of type Error so that the full stack trace information is available; plus, the TypeScript type definition assumes that the error object is always instanceof Error. Instead, failed responses reject with a FetchError, with the underlying Response object available as error.response. This is a backward-incompatible change: users who expected `error` to be of type Response now have to refer to `error.response` instead. * FetchError: add status code to the error message. * Define FetchError as a class, not just an interface. This is necessary for TypeScript code to be permitted to use FetchError as a value at runtime, e.g. to perform an `instanceof FetchError` check.
1 parent b678e3c commit 428256a

File tree

4 files changed

+32
-5
lines changed

4 files changed

+32
-5
lines changed

packages/react-async/src/index.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,8 @@ type FetchRun<T> = {
234234
run(): void
235235
}
236236

237+
export class FetchError extends Error {
238+
response: Response
239+
}
240+
237241
export default Async

packages/react-async/src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Async from "./Async"
22
export { default as Async, createInstance } from "./Async"
3-
export { default as useAsync, useFetch } from "./useAsync"
3+
export { default as useAsync, useFetch, FetchError } from "./useAsync"
44
export default Async
55
export { statusTypes } from "./status"
66
export { default as globalScope } from "./globalScope"

packages/react-async/src/useAsync.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,15 @@ const useAsync = (arg1, arg2) => {
154154
)
155155
}
156156

157+
export class FetchError extends Error {
158+
constructor(response) {
159+
super(`${response.status} ${response.statusText}`)
160+
this.response = response
161+
}
162+
}
163+
157164
const parseResponse = (accept, json) => res => {
158-
if (!res.ok) return Promise.reject(res)
165+
if (!res.ok) return Promise.reject(new FetchError(res))
159166
if (typeof json === "boolean") return json ? res.json() : res
160167
return accept === "application/json" ? res.json() : res
161168
}

packages/react-async/src/useAsync.spec.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import "@testing-library/jest-dom/extend-expect"
44
import React from "react"
55
import { render, fireEvent, cleanup } from "@testing-library/react"
6-
import { useAsync, useFetch, globalScope } from "./index"
6+
import { useAsync, useFetch, globalScope, FetchError } from "./index"
77
import {
88
sleep,
99
resolveTo,
@@ -20,11 +20,11 @@ const abortCtrl = { abort: jest.fn(), signal: "SIGNAL" }
2020
globalScope.AbortController = jest.fn(() => abortCtrl)
2121

2222
const json = jest.fn(() => ({}))
23-
globalScope.fetch = jest.fn(() => Promise.resolve({ ok: true, json }))
23+
globalScope.fetch = jest.fn()
2424

2525
beforeEach(abortCtrl.abort.mockClear)
26-
beforeEach(globalScope.fetch.mockClear)
2726
beforeEach(json.mockClear)
27+
beforeEach(() => globalScope.fetch.mockReset().mockResolvedValue({ ok: true, json }))
2828
afterEach(cleanup)
2929

3030
const Async = ({ children = () => null, ...props }) => children(useAsync(props))
@@ -250,4 +250,20 @@ describe("useFetch", () => {
250250
expect.objectContaining({ preventDefault: expect.any(Function) })
251251
)
252252
})
253+
254+
test("throws a FetchError for failed requests", async () => {
255+
const errorResponse = { ok: false, status: 400, statusText: "Bad Request", json }
256+
globalScope.fetch.mockResolvedValue(errorResponse)
257+
const onResolve = jest.fn()
258+
const onReject = jest.fn()
259+
render(<Fetch input="/test" options={{ onResolve, onReject }} />)
260+
expect(globalScope.fetch).toHaveBeenCalled()
261+
await sleep(10)
262+
expect(onResolve).not.toHaveBeenCalled()
263+
expect(onReject).toHaveBeenCalled()
264+
let [err] = onReject.mock.calls[0]
265+
expect(err).toBeInstanceOf(FetchError)
266+
expect(err.message).toEqual("400 Bad Request")
267+
expect(err.response).toBe(errorResponse)
268+
})
253269
})

0 commit comments

Comments
 (0)