use-fetch-error 1.0.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +162 -20
- package/dist/cjs/errors.d.ts +18 -0
- package/dist/cjs/errors.js +29 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/helpers.d.ts +19 -0
- package/dist/cjs/helpers.js +40 -0
- package/dist/cjs/helpers.js.map +1 -0
- package/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/index.js +12 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/types.d.ts +13 -0
- package/dist/cjs/types.js +3 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/useFetchError.d.ts +14 -0
- package/dist/cjs/useFetchError.js +87 -0
- package/dist/cjs/useFetchError.js.map +1 -0
- package/dist/esm/errors.d.ts +18 -0
- package/dist/esm/errors.js +24 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/helpers.d.ts +19 -0
- package/dist/esm/helpers.js +35 -0
- package/dist/esm/helpers.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/types.d.ts +13 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/useFetchError.d.ts +14 -0
- package/dist/esm/useFetchError.js +84 -0
- package/dist/esm/useFetchError.js.map +1 -0
- package/package.json +39 -3
- package/src/errors.ts +32 -0
- package/src/helpers.ts +41 -0
- package/src/index.ts +4 -80
- package/src/types.ts +23 -0
- package/src/useFetchError.ts +91 -0
- package/src/index.js +0 -58
package/README.md
CHANGED
|
@@ -1,20 +1,162 @@
|
|
|
1
|
-
# Use-fetch-error
|
|
2
|
-
|
|
3
|
-
## A library for error handling with [error, result] tuples using a `useFetchError()` function.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
# Use-fetch-error
|
|
2
|
+
|
|
3
|
+
## A library for error handling with [error, result] tuples using a `useFetchError()` function.
|
|
4
|
+
|
|
5
|
+
Works with **JavaScript and TypeScript**, as ES modules (`import`) or CommonJS (`require`). It does not modify any built-in prototype and has no dependencies.
|
|
6
|
+
|
|
7
|
+
### Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install use-fetch-error
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Example usage (JavaScript)
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
import { useFetchError } from "use-fetch-error";
|
|
17
|
+
// or: const { useFetchError } = require("use-fetch-error");
|
|
18
|
+
|
|
19
|
+
const [error, data] = await useFetchError("https://jsonplaceholder.typicode.com/posts/1");
|
|
20
|
+
|
|
21
|
+
if (error) {
|
|
22
|
+
console.error("An error occurred:", error);
|
|
23
|
+
} else {
|
|
24
|
+
console.log("Data received:", data);
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Example usage (TypeScript)
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { useFetchError, HttpError } from "use-fetch-error";
|
|
32
|
+
|
|
33
|
+
interface Post {
|
|
34
|
+
id: number;
|
|
35
|
+
title: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const [error, post] = await useFetchError<Post>("https://jsonplaceholder.typicode.com/posts/1");
|
|
39
|
+
|
|
40
|
+
if (error) {
|
|
41
|
+
if (error instanceof HttpError) {
|
|
42
|
+
console.error(error.status, error.body);
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
console.log(post.title); // `post` is `Post` here, not `Post | undefined`
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### When is `error` set?
|
|
50
|
+
|
|
51
|
+
| Case | `error` |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| Network failure / aborted request | the `fetch` error |
|
|
54
|
+
| `timeout` expired | `TimeoutError` (`error.timeout`) |
|
|
55
|
+
| Status outside 200-299 | `HttpError` (`error.status`, `error.statusText`, `error.body`, `error.response`) |
|
|
56
|
+
| Body cannot be parsed | the parse error (e.g. `SyntaxError`) |
|
|
57
|
+
|
|
58
|
+
An empty body (e.g. `204 No Content`) returns `[null, null]`.
|
|
59
|
+
|
|
60
|
+
### Options
|
|
61
|
+
|
|
62
|
+
`useFetchError(input, options)` accepts every `fetch` option plus:
|
|
63
|
+
|
|
64
|
+
- `parseAs`: `"json"` (default), `"text"`, `"blob"`, `"arrayBuffer"`, `"formData"` or `"response"` (returns the `Response` untouched).
|
|
65
|
+
- `timeout`: milliseconds before the request is aborted.
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
const [error, html] = await useFetchError("/page", { parseAs: "text", timeout: 5000 });
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Other helpers: `to()` and `tryCatch()`
|
|
72
|
+
|
|
73
|
+
`useFetchError` is built on two small helpers, and they are exported so you can use the same `[error, data]` style for anything that is not an HTTP request (database queries, file reads, `JSON.parse`...). You don't need them to use `useFetchError`.
|
|
74
|
+
|
|
75
|
+
Both catch the error instead of letting it be thrown, and return a tuple:
|
|
76
|
+
|
|
77
|
+
- `[null, data]` if everything went well.
|
|
78
|
+
- `[error, undefined]` if something failed.
|
|
79
|
+
|
|
80
|
+
#### `to(promise)`: for async code
|
|
81
|
+
|
|
82
|
+
Takes any promise (or anything you would `await`) and waits for it.
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
import { to } from "use-fetch-error";
|
|
86
|
+
|
|
87
|
+
const [error, user] = await to(db.users.findById(id));
|
|
88
|
+
|
|
89
|
+
if (error) {
|
|
90
|
+
console.error("Could not load the user:", error);
|
|
91
|
+
} else {
|
|
92
|
+
console.log(user.name);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
It is the same as writing:
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
let error = null;
|
|
100
|
+
let user;
|
|
101
|
+
try {
|
|
102
|
+
user = await db.users.findById(id);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
error = e;
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
More examples:
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
// Read a file (Node.js)
|
|
112
|
+
import { readFile } from "node:fs/promises";
|
|
113
|
+
const [readError, content] = await to(readFile("config.json", "utf8"));
|
|
114
|
+
|
|
115
|
+
// Several requests at once: fails if any of them fails
|
|
116
|
+
const [error, [posts, users] = []] = await to(Promise.all([getPosts(), getUsers()]));
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
#### `tryCatch(fn, ...args)`: for sync code
|
|
120
|
+
|
|
121
|
+
Calls `fn(...args)` for you and catches anything it throws.
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
import { tryCatch } from "use-fetch-error";
|
|
125
|
+
|
|
126
|
+
tryCatch(JSON.parse, '{"port": 3000}'); // → [null, { port: 3000 }]
|
|
127
|
+
tryCatch(JSON.parse, "{ not json"); // → [SyntaxError, undefined]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Pass the function and its arguments **separately**: `tryCatch(JSON.parse, text)`, not `tryCatch(JSON.parse(text))`. In the second form `JSON.parse(text)` runs, and throws, before `tryCatch` can catch anything.
|
|
131
|
+
|
|
132
|
+
When you need more than a single call, pass an arrow function with no arguments:
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
const [error, url] = tryCatch(() => new URL(input, "https://example.com"));
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### With TypeScript
|
|
139
|
+
|
|
140
|
+
Types are inferred, and checking the error narrows the data:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { to, tryCatch } from "use-fetch-error";
|
|
144
|
+
|
|
145
|
+
const [error, user] = await to(getUser(id)); // user: User | undefined
|
|
146
|
+
if (error) throw error;
|
|
147
|
+
user.name; // user: User
|
|
148
|
+
|
|
149
|
+
// The error is typed as `Error`; you can be more specific
|
|
150
|
+
const [parseError, data] = tryCatch<[string], Config, SyntaxError>(JSON.parse, text);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The error is always an `Error`: if the code throws something else (for example `throw "oops"`), it is wrapped in `new Error("oops")`.
|
|
154
|
+
|
|
155
|
+
### Development
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
npm install
|
|
159
|
+
npm test
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`npm test` type-checks, builds `dist/esm` and `dist/cjs` (with `.d.ts` files) and runs the tests against both builds.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error returned when the server answers with a status outside 200-299.
|
|
3
|
+
*/
|
|
4
|
+
export declare class HttpError extends Error {
|
|
5
|
+
readonly status: number;
|
|
6
|
+
readonly statusText: string;
|
|
7
|
+
readonly response: Response;
|
|
8
|
+
/** Response body (parsed as JSON when possible, otherwise text). */
|
|
9
|
+
readonly body: unknown;
|
|
10
|
+
constructor(response: Response, body: unknown);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Error returned when the request is aborted because `timeout` expired.
|
|
14
|
+
*/
|
|
15
|
+
export declare class TimeoutError extends Error {
|
|
16
|
+
readonly timeout: number;
|
|
17
|
+
constructor(timeout: number);
|
|
18
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TimeoutError = exports.HttpError = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Error returned when the server answers with a status outside 200-299.
|
|
6
|
+
*/
|
|
7
|
+
class HttpError extends Error {
|
|
8
|
+
constructor(response, body) {
|
|
9
|
+
super(`HTTP ${response.status} ${response.statusText}`.trim());
|
|
10
|
+
this.name = "HttpError";
|
|
11
|
+
this.status = response.status;
|
|
12
|
+
this.statusText = response.statusText;
|
|
13
|
+
this.response = response;
|
|
14
|
+
this.body = body;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.HttpError = HttpError;
|
|
18
|
+
/**
|
|
19
|
+
* Error returned when the request is aborted because `timeout` expired.
|
|
20
|
+
*/
|
|
21
|
+
class TimeoutError extends Error {
|
|
22
|
+
constructor(timeout) {
|
|
23
|
+
super(`Request timed out after ${timeout} ms`);
|
|
24
|
+
this.name = "TimeoutError";
|
|
25
|
+
this.timeout = timeout;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.TimeoutError = TimeoutError;
|
|
29
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":";;;AAAA;;GAEG;AACH,MAAa,SAAU,SAAQ,KAAK;IAOlC,YAAY,QAAkB,EAAE,IAAa;QAC3C,KAAK,CAAC,QAAQ,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;QAC/D,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;QACxB,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;QAC9B,IAAI,CAAC,UAAU,GAAG,QAAQ,CAAC,UAAU,CAAC;QACtC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAfD,8BAeC;AAED;;GAEG;AACH,MAAa,YAAa,SAAQ,KAAK;IAGrC,YAAY,OAAe;QACzB,KAAK,CAAC,2BAA2B,OAAO,KAAK,CAAC,CAAC;QAC/C,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF;AARD,oCAQC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Result } from "./types.js";
|
|
2
|
+
/** Returns `value` if it is an `Error`, otherwise wraps it in one. */
|
|
3
|
+
export declare function toError(value: unknown): Error;
|
|
4
|
+
/**
|
|
5
|
+
* Awaits a promise and returns a tuple `[error, data]` instead of throwing.
|
|
6
|
+
* Thrown values that are not an `Error` are wrapped in one.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const [error, user] = await to(getUser(id));
|
|
10
|
+
*/
|
|
11
|
+
export declare function to<T, E extends Error = Error>(promise: PromiseLike<T>): Promise<Result<T, E>>;
|
|
12
|
+
/**
|
|
13
|
+
* Runs a synchronous function and returns a tuple `[error, data]` instead of throwing.
|
|
14
|
+
* Thrown values that are not an `Error` are wrapped in one.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const [error, config] = tryCatch(JSON.parse, text);
|
|
18
|
+
*/
|
|
19
|
+
export declare function tryCatch<A extends unknown[], R, E extends Error = Error>(fn: (...args: A) => R, ...args: A): Result<R, E>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toError = toError;
|
|
4
|
+
exports.to = to;
|
|
5
|
+
exports.tryCatch = tryCatch;
|
|
6
|
+
/** Returns `value` if it is an `Error`, otherwise wraps it in one. */
|
|
7
|
+
function toError(value) {
|
|
8
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Awaits a promise and returns a tuple `[error, data]` instead of throwing.
|
|
12
|
+
* Thrown values that are not an `Error` are wrapped in one.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const [error, user] = await to(getUser(id));
|
|
16
|
+
*/
|
|
17
|
+
async function to(promise) {
|
|
18
|
+
try {
|
|
19
|
+
return [null, await promise];
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return [toError(error), undefined];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Runs a synchronous function and returns a tuple `[error, data]` instead of throwing.
|
|
27
|
+
* Thrown values that are not an `Error` are wrapped in one.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const [error, config] = tryCatch(JSON.parse, text);
|
|
31
|
+
*/
|
|
32
|
+
function tryCatch(fn, ...args) {
|
|
33
|
+
try {
|
|
34
|
+
return [null, fn(...args)];
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
return [toError(error), undefined];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../src/helpers.ts"],"names":[],"mappings":";;AAGA,0BAEC;AASD,gBAQC;AASD,4BASC;AAtCD,sEAAsE;AACtE,SAAgB,OAAO,CAAC,KAAc;IACpC,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;AACnE,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,EAAE,CACtB,OAAuB;IAEvB,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC;IAC/B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,OAAO,CAAC,KAAK,CAAM,EAAE,SAAS,CAAC,CAAC;IAC1C,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,QAAQ,CACtB,EAAqB,EACrB,GAAG,IAAO;IAEV,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IAC7B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,OAAO,CAAC,KAAK,CAAM,EAAE,SAAS,CAAC,CAAC;IAC1C,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.tryCatch = exports.to = exports.TimeoutError = exports.HttpError = exports.useFetchError = void 0;
|
|
4
|
+
var useFetchError_js_1 = require("./useFetchError.js");
|
|
5
|
+
Object.defineProperty(exports, "useFetchError", { enumerable: true, get: function () { return useFetchError_js_1.useFetchError; } });
|
|
6
|
+
var errors_js_1 = require("./errors.js");
|
|
7
|
+
Object.defineProperty(exports, "HttpError", { enumerable: true, get: function () { return errors_js_1.HttpError; } });
|
|
8
|
+
Object.defineProperty(exports, "TimeoutError", { enumerable: true, get: function () { return errors_js_1.TimeoutError; } });
|
|
9
|
+
var helpers_js_1 = require("./helpers.js");
|
|
10
|
+
Object.defineProperty(exports, "to", { enumerable: true, get: function () { return helpers_js_1.to; } });
|
|
11
|
+
Object.defineProperty(exports, "tryCatch", { enumerable: true, get: function () { return helpers_js_1.tryCatch; } });
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;AAAA,uDAAmD;AAA1C,iHAAA,aAAa,OAAA;AACtB,yCAAsD;AAA7C,sGAAA,SAAS,OAAA;AAAE,yGAAA,YAAY,OAAA;AAChC,2CAA4C;AAAnC,gGAAA,EAAE,OAAA;AAAE,sGAAA,QAAQ,OAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tuple `[error, data]`. When `error` is `null`, `data` holds the value;
|
|
3
|
+
* otherwise `data` is `undefined`. Checking `error` narrows the type of `data`.
|
|
4
|
+
*/
|
|
5
|
+
export type Result<T, E = unknown> = [error: null, data: T] | [error: E, data: undefined];
|
|
6
|
+
/** How the response body is read when the request succeeds. */
|
|
7
|
+
export type ParseAs = "json" | "text" | "blob" | "arrayBuffer" | "formData" | "response";
|
|
8
|
+
export interface UseFetchErrorOptions extends RequestInit {
|
|
9
|
+
/** How to read the response body. Defaults to `"json"`. */
|
|
10
|
+
parseAs?: ParseAs;
|
|
11
|
+
/** Aborts the request after this many milliseconds. */
|
|
12
|
+
timeout?: number;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Result, UseFetchErrorOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Makes an HTTP request with `fetch` and never throws: it returns `[error, data]`.
|
|
4
|
+
*
|
|
5
|
+
* `error` is set when there is a network error, a timeout or abort, an HTTP
|
|
6
|
+
* status outside 200-299 (`HttpError`), or the body cannot be parsed.
|
|
7
|
+
*
|
|
8
|
+
* @template T - Expected type of the parsed body.
|
|
9
|
+
* @example
|
|
10
|
+
* const [error, post] = await useFetchError<Post>("/api/posts/1");
|
|
11
|
+
* if (error) return console.error(error);
|
|
12
|
+
* console.log(post.title);
|
|
13
|
+
*/
|
|
14
|
+
export declare function useFetchError<T = unknown>(input: RequestInfo | URL, options?: UseFetchErrorOptions): Promise<Result<T, Error>>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useFetchError = useFetchError;
|
|
4
|
+
const errors_js_1 = require("./errors.js");
|
|
5
|
+
const helpers_js_1 = require("./helpers.js");
|
|
6
|
+
async function readBody(response, parseAs) {
|
|
7
|
+
switch (parseAs) {
|
|
8
|
+
case "response":
|
|
9
|
+
return response;
|
|
10
|
+
case "text":
|
|
11
|
+
return response.text();
|
|
12
|
+
case "blob":
|
|
13
|
+
return response.blob();
|
|
14
|
+
case "arrayBuffer":
|
|
15
|
+
return response.arrayBuffer();
|
|
16
|
+
case "formData":
|
|
17
|
+
return response.formData();
|
|
18
|
+
case "json": {
|
|
19
|
+
// Empty bodies (204, 205, HEAD...) resolve to `null` instead of failing.
|
|
20
|
+
const text = await response.text();
|
|
21
|
+
return text.trim() === "" ? null : JSON.parse(text);
|
|
22
|
+
}
|
|
23
|
+
default:
|
|
24
|
+
throw new TypeError(`Unsupported parseAs value: ${String(parseAs)}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function readErrorBody(response) {
|
|
28
|
+
const [, text] = await (0, helpers_js_1.to)(response.text());
|
|
29
|
+
if (text === undefined || text === "")
|
|
30
|
+
return undefined;
|
|
31
|
+
const [parseError, json] = (0, helpers_js_1.tryCatch)(JSON.parse, text);
|
|
32
|
+
return parseError ? text : json;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Makes an HTTP request with `fetch` and never throws: it returns `[error, data]`.
|
|
36
|
+
*
|
|
37
|
+
* `error` is set when there is a network error, a timeout or abort, an HTTP
|
|
38
|
+
* status outside 200-299 (`HttpError`), or the body cannot be parsed.
|
|
39
|
+
*
|
|
40
|
+
* @template T - Expected type of the parsed body.
|
|
41
|
+
* @example
|
|
42
|
+
* const [error, post] = await useFetchError<Post>("/api/posts/1");
|
|
43
|
+
* if (error) return console.error(error);
|
|
44
|
+
* console.log(post.title);
|
|
45
|
+
*/
|
|
46
|
+
async function useFetchError(input, options = {}) {
|
|
47
|
+
const { parseAs = "json", timeout, ...init } = options;
|
|
48
|
+
let timer;
|
|
49
|
+
let timedOut = false;
|
|
50
|
+
let onAbort;
|
|
51
|
+
if (timeout !== undefined) {
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
const userSignal = init.signal;
|
|
54
|
+
if (userSignal) {
|
|
55
|
+
if (userSignal.aborted)
|
|
56
|
+
controller.abort(userSignal.reason);
|
|
57
|
+
onAbort = () => controller.abort(userSignal.reason);
|
|
58
|
+
userSignal.addEventListener("abort", onAbort, { once: true });
|
|
59
|
+
}
|
|
60
|
+
timer = setTimeout(() => {
|
|
61
|
+
timedOut = true;
|
|
62
|
+
controller.abort();
|
|
63
|
+
}, timeout);
|
|
64
|
+
init.signal = controller.signal;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const [fetchError, response] = await (0, helpers_js_1.to)(fetch(input, init));
|
|
68
|
+
if (fetchError || !response) {
|
|
69
|
+
return [timedOut ? new errors_js_1.TimeoutError(timeout) : (0, helpers_js_1.toError)(fetchError), undefined];
|
|
70
|
+
}
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
return [new errors_js_1.HttpError(response, await readErrorBody(response)), undefined];
|
|
73
|
+
}
|
|
74
|
+
const [parseError, data] = await (0, helpers_js_1.to)(readBody(response, parseAs));
|
|
75
|
+
if (parseError) {
|
|
76
|
+
return [timedOut ? new errors_js_1.TimeoutError(timeout) : (0, helpers_js_1.toError)(parseError), undefined];
|
|
77
|
+
}
|
|
78
|
+
return [null, data];
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
if (timer !== undefined)
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
if (onAbort)
|
|
84
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=useFetchError.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFetchError.js","sourceRoot":"","sources":["../../src/useFetchError.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error returned when the server answers with a status outside 200-299.
|
|
3
|
+
*/
|
|
4
|
+
export declare class HttpError extends Error {
|
|
5
|
+
readonly status: number;
|
|
6
|
+
readonly statusText: string;
|
|
7
|
+
readonly response: Response;
|
|
8
|
+
/** Response body (parsed as JSON when possible, otherwise text). */
|
|
9
|
+
readonly body: unknown;
|
|
10
|
+
constructor(response: Response, body: unknown);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Error returned when the request is aborted because `timeout` expired.
|
|
14
|
+
*/
|
|
15
|
+
export declare class TimeoutError extends Error {
|
|
16
|
+
readonly timeout: number;
|
|
17
|
+
constructor(timeout: number);
|
|
18
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error returned when the server answers with a status outside 200-299.
|
|
3
|
+
*/
|
|
4
|
+
export class HttpError extends Error {
|
|
5
|
+
constructor(response, body) {
|
|
6
|
+
super(`HTTP ${response.status} ${response.statusText}`.trim());
|
|
7
|
+
this.name = "HttpError";
|
|
8
|
+
this.status = response.status;
|
|
9
|
+
this.statusText = response.statusText;
|
|
10
|
+
this.response = response;
|
|
11
|
+
this.body = body;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Error returned when the request is aborted because `timeout` expired.
|
|
16
|
+
*/
|
|
17
|
+
export class TimeoutError extends Error {
|
|
18
|
+
constructor(timeout) {
|
|
19
|
+
super(`Request timed out after ${timeout} ms`);
|
|
20
|
+
this.name = "TimeoutError";
|
|
21
|
+
this.timeout = timeout;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,OAAO,SAAU,SAAQ,KAAK;IAOlC,YAAY,QAAkB,EAAE,IAAa;QAC3C,KAAK,CAAC,QAAQ,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;QAC/D,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;QACxB,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;QAC9B,IAAI,CAAC,UAAU,GAAG,QAAQ,CAAC,UAAU,CAAC;QACtC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED;;GAEG;AACH,MAAM,OAAO,YAAa,SAAQ,KAAK;IAGrC,YAAY,OAAe;QACzB,KAAK,CAAC,2BAA2B,OAAO,KAAK,CAAC,CAAC;QAC/C,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Result } from "./types.js";
|
|
2
|
+
/** Returns `value` if it is an `Error`, otherwise wraps it in one. */
|
|
3
|
+
export declare function toError(value: unknown): Error;
|
|
4
|
+
/**
|
|
5
|
+
* Awaits a promise and returns a tuple `[error, data]` instead of throwing.
|
|
6
|
+
* Thrown values that are not an `Error` are wrapped in one.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const [error, user] = await to(getUser(id));
|
|
10
|
+
*/
|
|
11
|
+
export declare function to<T, E extends Error = Error>(promise: PromiseLike<T>): Promise<Result<T, E>>;
|
|
12
|
+
/**
|
|
13
|
+
* Runs a synchronous function and returns a tuple `[error, data]` instead of throwing.
|
|
14
|
+
* Thrown values that are not an `Error` are wrapped in one.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const [error, config] = tryCatch(JSON.parse, text);
|
|
18
|
+
*/
|
|
19
|
+
export declare function tryCatch<A extends unknown[], R, E extends Error = Error>(fn: (...args: A) => R, ...args: A): Result<R, E>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Returns `value` if it is an `Error`, otherwise wraps it in one. */
|
|
2
|
+
export function toError(value) {
|
|
3
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Awaits a promise and returns a tuple `[error, data]` instead of throwing.
|
|
7
|
+
* Thrown values that are not an `Error` are wrapped in one.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const [error, user] = await to(getUser(id));
|
|
11
|
+
*/
|
|
12
|
+
export async function to(promise) {
|
|
13
|
+
try {
|
|
14
|
+
return [null, await promise];
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
return [toError(error), undefined];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Runs a synchronous function and returns a tuple `[error, data]` instead of throwing.
|
|
22
|
+
* Thrown values that are not an `Error` are wrapped in one.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* const [error, config] = tryCatch(JSON.parse, text);
|
|
26
|
+
*/
|
|
27
|
+
export function tryCatch(fn, ...args) {
|
|
28
|
+
try {
|
|
29
|
+
return [null, fn(...args)];
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
return [toError(error), undefined];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../src/helpers.ts"],"names":[],"mappings":"AAEA,sEAAsE;AACtE,MAAM,UAAU,OAAO,CAAC,KAAc;IACpC,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;AACnE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,EAAE,CACtB,OAAuB;IAEvB,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC;IAC/B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,OAAO,CAAC,KAAK,CAAM,EAAE,SAAS,CAAC,CAAC;IAC1C,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CACtB,EAAqB,EACrB,GAAG,IAAO;IAEV,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IAC7B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,OAAO,CAAC,KAAK,CAAM,EAAE,SAAS,CAAC,CAAC;IAC1C,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tuple `[error, data]`. When `error` is `null`, `data` holds the value;
|
|
3
|
+
* otherwise `data` is `undefined`. Checking `error` narrows the type of `data`.
|
|
4
|
+
*/
|
|
5
|
+
export type Result<T, E = unknown> = [error: null, data: T] | [error: E, data: undefined];
|
|
6
|
+
/** How the response body is read when the request succeeds. */
|
|
7
|
+
export type ParseAs = "json" | "text" | "blob" | "arrayBuffer" | "formData" | "response";
|
|
8
|
+
export interface UseFetchErrorOptions extends RequestInit {
|
|
9
|
+
/** How to read the response body. Defaults to `"json"`. */
|
|
10
|
+
parseAs?: ParseAs;
|
|
11
|
+
/** Aborts the request after this many milliseconds. */
|
|
12
|
+
timeout?: number;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Result, UseFetchErrorOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Makes an HTTP request with `fetch` and never throws: it returns `[error, data]`.
|
|
4
|
+
*
|
|
5
|
+
* `error` is set when there is a network error, a timeout or abort, an HTTP
|
|
6
|
+
* status outside 200-299 (`HttpError`), or the body cannot be parsed.
|
|
7
|
+
*
|
|
8
|
+
* @template T - Expected type of the parsed body.
|
|
9
|
+
* @example
|
|
10
|
+
* const [error, post] = await useFetchError<Post>("/api/posts/1");
|
|
11
|
+
* if (error) return console.error(error);
|
|
12
|
+
* console.log(post.title);
|
|
13
|
+
*/
|
|
14
|
+
export declare function useFetchError<T = unknown>(input: RequestInfo | URL, options?: UseFetchErrorOptions): Promise<Result<T, Error>>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { HttpError, TimeoutError } from "./errors.js";
|
|
2
|
+
import { to, toError, tryCatch } from "./helpers.js";
|
|
3
|
+
async function readBody(response, parseAs) {
|
|
4
|
+
switch (parseAs) {
|
|
5
|
+
case "response":
|
|
6
|
+
return response;
|
|
7
|
+
case "text":
|
|
8
|
+
return response.text();
|
|
9
|
+
case "blob":
|
|
10
|
+
return response.blob();
|
|
11
|
+
case "arrayBuffer":
|
|
12
|
+
return response.arrayBuffer();
|
|
13
|
+
case "formData":
|
|
14
|
+
return response.formData();
|
|
15
|
+
case "json": {
|
|
16
|
+
// Empty bodies (204, 205, HEAD...) resolve to `null` instead of failing.
|
|
17
|
+
const text = await response.text();
|
|
18
|
+
return text.trim() === "" ? null : JSON.parse(text);
|
|
19
|
+
}
|
|
20
|
+
default:
|
|
21
|
+
throw new TypeError(`Unsupported parseAs value: ${String(parseAs)}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function readErrorBody(response) {
|
|
25
|
+
const [, text] = await to(response.text());
|
|
26
|
+
if (text === undefined || text === "")
|
|
27
|
+
return undefined;
|
|
28
|
+
const [parseError, json] = tryCatch(JSON.parse, text);
|
|
29
|
+
return parseError ? text : json;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Makes an HTTP request with `fetch` and never throws: it returns `[error, data]`.
|
|
33
|
+
*
|
|
34
|
+
* `error` is set when there is a network error, a timeout or abort, an HTTP
|
|
35
|
+
* status outside 200-299 (`HttpError`), or the body cannot be parsed.
|
|
36
|
+
*
|
|
37
|
+
* @template T - Expected type of the parsed body.
|
|
38
|
+
* @example
|
|
39
|
+
* const [error, post] = await useFetchError<Post>("/api/posts/1");
|
|
40
|
+
* if (error) return console.error(error);
|
|
41
|
+
* console.log(post.title);
|
|
42
|
+
*/
|
|
43
|
+
export async function useFetchError(input, options = {}) {
|
|
44
|
+
const { parseAs = "json", timeout, ...init } = options;
|
|
45
|
+
let timer;
|
|
46
|
+
let timedOut = false;
|
|
47
|
+
let onAbort;
|
|
48
|
+
if (timeout !== undefined) {
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
const userSignal = init.signal;
|
|
51
|
+
if (userSignal) {
|
|
52
|
+
if (userSignal.aborted)
|
|
53
|
+
controller.abort(userSignal.reason);
|
|
54
|
+
onAbort = () => controller.abort(userSignal.reason);
|
|
55
|
+
userSignal.addEventListener("abort", onAbort, { once: true });
|
|
56
|
+
}
|
|
57
|
+
timer = setTimeout(() => {
|
|
58
|
+
timedOut = true;
|
|
59
|
+
controller.abort();
|
|
60
|
+
}, timeout);
|
|
61
|
+
init.signal = controller.signal;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const [fetchError, response] = await to(fetch(input, init));
|
|
65
|
+
if (fetchError || !response) {
|
|
66
|
+
return [timedOut ? new TimeoutError(timeout) : toError(fetchError), undefined];
|
|
67
|
+
}
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
return [new HttpError(response, await readErrorBody(response)), undefined];
|
|
70
|
+
}
|
|
71
|
+
const [parseError, data] = await to(readBody(response, parseAs));
|
|
72
|
+
if (parseError) {
|
|
73
|
+
return [timedOut ? new TimeoutError(timeout) : toError(parseError), undefined];
|
|
74
|
+
}
|
|
75
|
+
return [null, data];
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
if (timer !== undefined)
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
if (onAbort)
|
|
81
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=useFetchError.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFetchError.js","sourceRoot":"","sources":["../../src/useFetchError.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,14 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "use-fetch-error",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A library for error handling with [error, result] tuples using a useFetchError() function.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"javascript",
|
|
7
|
+
"typescript",
|
|
7
8
|
"fetch",
|
|
8
9
|
"handle error"
|
|
9
10
|
],
|
|
10
11
|
"author": "llatzer",
|
|
11
12
|
"license": "MIT",
|
|
12
|
-
"
|
|
13
|
-
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/llatzer-dev/use-fetch-error.git"
|
|
16
|
+
},
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"main": "./dist/cjs/index.js",
|
|
19
|
+
"module": "./dist/esm/index.js",
|
|
20
|
+
"types": "./dist/cjs/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": {
|
|
24
|
+
"types": "./dist/esm/index.d.ts",
|
|
25
|
+
"default": "./dist/esm/index.js"
|
|
26
|
+
},
|
|
27
|
+
"require": {
|
|
28
|
+
"types": "./dist/cjs/index.d.ts",
|
|
29
|
+
"default": "./dist/cjs/index.js"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"src"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
42
|
+
"build": "npm run clean && tsc -p tsconfig.esm.json && tsc -p tsconfig.cjs.json && node scripts/write-dist-packages.mjs",
|
|
43
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
44
|
+
"test": "npm run typecheck && npm run build && node --test test/esm.test.mjs test/cjs.test.cjs",
|
|
45
|
+
"prepublishOnly": "npm test"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"typescript": "^5.6.2"
|
|
49
|
+
}
|
|
14
50
|
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error returned when the server answers with a status outside 200-299.
|
|
3
|
+
*/
|
|
4
|
+
export class HttpError extends Error {
|
|
5
|
+
readonly status: number;
|
|
6
|
+
readonly statusText: string;
|
|
7
|
+
readonly response: Response;
|
|
8
|
+
/** Response body (parsed as JSON when possible, otherwise text). */
|
|
9
|
+
readonly body: unknown;
|
|
10
|
+
|
|
11
|
+
constructor(response: Response, body: unknown) {
|
|
12
|
+
super(`HTTP ${response.status} ${response.statusText}`.trim());
|
|
13
|
+
this.name = "HttpError";
|
|
14
|
+
this.status = response.status;
|
|
15
|
+
this.statusText = response.statusText;
|
|
16
|
+
this.response = response;
|
|
17
|
+
this.body = body;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Error returned when the request is aborted because `timeout` expired.
|
|
23
|
+
*/
|
|
24
|
+
export class TimeoutError extends Error {
|
|
25
|
+
readonly timeout: number;
|
|
26
|
+
|
|
27
|
+
constructor(timeout: number) {
|
|
28
|
+
super(`Request timed out after ${timeout} ms`);
|
|
29
|
+
this.name = "TimeoutError";
|
|
30
|
+
this.timeout = timeout;
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Result } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/** Returns `value` if it is an `Error`, otherwise wraps it in one. */
|
|
4
|
+
export function toError(value: unknown): Error {
|
|
5
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Awaits a promise and returns a tuple `[error, data]` instead of throwing.
|
|
10
|
+
* Thrown values that are not an `Error` are wrapped in one.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const [error, user] = await to(getUser(id));
|
|
14
|
+
*/
|
|
15
|
+
export async function to<T, E extends Error = Error>(
|
|
16
|
+
promise: PromiseLike<T>
|
|
17
|
+
): Promise<Result<T, E>> {
|
|
18
|
+
try {
|
|
19
|
+
return [null, await promise];
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return [toError(error) as E, undefined];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Runs a synchronous function and returns a tuple `[error, data]` instead of throwing.
|
|
27
|
+
* Thrown values that are not an `Error` are wrapped in one.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const [error, config] = tryCatch(JSON.parse, text);
|
|
31
|
+
*/
|
|
32
|
+
export function tryCatch<A extends unknown[], R, E extends Error = Error>(
|
|
33
|
+
fn: (...args: A) => R,
|
|
34
|
+
...args: A
|
|
35
|
+
): Result<R, E> {
|
|
36
|
+
try {
|
|
37
|
+
return [null, fn(...args)];
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return [toError(error) as E, undefined];
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,80 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* to include a method that handles errors and returns a tuple
|
|
6
|
-
* containing the error and result.
|
|
7
|
-
*/
|
|
8
|
-
interface FunctionWithSymbol extends Function {
|
|
9
|
-
/**
|
|
10
|
-
* Executes the function and returns a tuple `[error, result]`.
|
|
11
|
-
* @param {...any[]} args - Arguments to pass to the function.
|
|
12
|
-
* @returns {[any, any]} A tuple containing `null` and the result if successful, or the error and `undefined` if it throws.
|
|
13
|
-
*/
|
|
14
|
-
[symbol](...args: any[]): [any, any];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
Function.prototype[symbol] = function (
|
|
18
|
-
this: FunctionWithSymbol,
|
|
19
|
-
...args: any[]
|
|
20
|
-
): [any, any] {
|
|
21
|
-
try {
|
|
22
|
-
const result = this.apply(this, args);
|
|
23
|
-
return [null, result];
|
|
24
|
-
} catch (error) {
|
|
25
|
-
return [error, undefined];
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Interface that extends the native `Promise` prototype
|
|
31
|
-
* to include a method that handles promise rejections and
|
|
32
|
-
* resolves to a tuple `[error, result]`.
|
|
33
|
-
*
|
|
34
|
-
* @template T - The type of the resolved value of the promise.
|
|
35
|
-
*/
|
|
36
|
-
interface PromiseWithSymbol<T> extends Promise<T> {
|
|
37
|
-
/**
|
|
38
|
-
* Resolves the promise and returns a tuple `[error, result]`.
|
|
39
|
-
* @returns {Promise<[any, T | undefined]>} A promise that resolves with a tuple containing `null` and the result if successful, or the error and `undefined` if rejected.
|
|
40
|
-
*/
|
|
41
|
-
[symbol](): Promise<[any, T | undefined]>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
Promise.prototype[symbol] = async function <T>(
|
|
45
|
-
this: PromiseWithSymbol<T>
|
|
46
|
-
): Promise<[any, T | undefined]> {
|
|
47
|
-
try {
|
|
48
|
-
const result = await this;
|
|
49
|
-
return [null, result];
|
|
50
|
-
} catch (error) {
|
|
51
|
-
return [error, undefined];
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Fetches a URL and handles both network errors and JSON parsing errors.
|
|
57
|
-
*
|
|
58
|
-
* @template T - The expected data type of the parsed JSON response.
|
|
59
|
-
* @param {string} url - The URL to fetch.
|
|
60
|
-
* @param {RequestInit} [options={}] - Optional configuration for the fetch request.
|
|
61
|
-
* @returns {Promise<[any, T | undefined]>} A promise that resolves with a tuple containing either the error and `undefined` if something went wrong, or `null` and the parsed JSON data if successful.
|
|
62
|
-
*/
|
|
63
|
-
export async function useFetchError<T>(
|
|
64
|
-
url: string,
|
|
65
|
-
options: RequestInit = {}
|
|
66
|
-
): Promise<[any, T | undefined]> {
|
|
67
|
-
const [error, response] = await fetch(url, options)[symbol]();
|
|
68
|
-
|
|
69
|
-
if (error) {
|
|
70
|
-
return [error, undefined]; // Retorna el error si ocurrió
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const [parseError, data] = await (response as Response).json()[symbol]();
|
|
74
|
-
|
|
75
|
-
if (parseError) {
|
|
76
|
-
return [parseError, undefined]; // Retorna el error al parsear el JSON
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return [null, data]; // Retorna el resultado si todo fue bien
|
|
80
|
-
}
|
|
1
|
+
export { useFetchError } from "./useFetchError.js";
|
|
2
|
+
export { HttpError, TimeoutError } from "./errors.js";
|
|
3
|
+
export { to, tryCatch } from "./helpers.js";
|
|
4
|
+
export type { Result, ParseAs, UseFetchErrorOptions } from "./types.js";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tuple `[error, data]`. When `error` is `null`, `data` holds the value;
|
|
3
|
+
* otherwise `data` is `undefined`. Checking `error` narrows the type of `data`.
|
|
4
|
+
*/
|
|
5
|
+
export type Result<T, E = unknown> =
|
|
6
|
+
| [error: null, data: T]
|
|
7
|
+
| [error: E, data: undefined];
|
|
8
|
+
|
|
9
|
+
/** How the response body is read when the request succeeds. */
|
|
10
|
+
export type ParseAs =
|
|
11
|
+
| "json"
|
|
12
|
+
| "text"
|
|
13
|
+
| "blob"
|
|
14
|
+
| "arrayBuffer"
|
|
15
|
+
| "formData"
|
|
16
|
+
| "response";
|
|
17
|
+
|
|
18
|
+
export interface UseFetchErrorOptions extends RequestInit {
|
|
19
|
+
/** How to read the response body. Defaults to `"json"`. */
|
|
20
|
+
parseAs?: ParseAs;
|
|
21
|
+
/** Aborts the request after this many milliseconds. */
|
|
22
|
+
timeout?: number;
|
|
23
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { HttpError, TimeoutError } from "./errors.js";
|
|
2
|
+
import { to, toError, tryCatch } from "./helpers.js";
|
|
3
|
+
import type { ParseAs, Result, UseFetchErrorOptions } from "./types.js";
|
|
4
|
+
|
|
5
|
+
async function readBody(response: Response, parseAs: ParseAs): Promise<unknown> {
|
|
6
|
+
switch (parseAs) {
|
|
7
|
+
case "response":
|
|
8
|
+
return response;
|
|
9
|
+
case "text":
|
|
10
|
+
return response.text();
|
|
11
|
+
case "blob":
|
|
12
|
+
return response.blob();
|
|
13
|
+
case "arrayBuffer":
|
|
14
|
+
return response.arrayBuffer();
|
|
15
|
+
case "formData":
|
|
16
|
+
return response.formData();
|
|
17
|
+
case "json": {
|
|
18
|
+
// Empty bodies (204, 205, HEAD...) resolve to `null` instead of failing.
|
|
19
|
+
const text = await response.text();
|
|
20
|
+
return text.trim() === "" ? null : JSON.parse(text);
|
|
21
|
+
}
|
|
22
|
+
default:
|
|
23
|
+
throw new TypeError(`Unsupported parseAs value: ${String(parseAs)}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function readErrorBody(response: Response): Promise<unknown> {
|
|
28
|
+
const [, text] = await to<string>(response.text());
|
|
29
|
+
if (text === undefined || text === "") return undefined;
|
|
30
|
+
const [parseError, json] = tryCatch(JSON.parse, text);
|
|
31
|
+
return parseError ? text : json;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Makes an HTTP request with `fetch` and never throws: it returns `[error, data]`.
|
|
36
|
+
*
|
|
37
|
+
* `error` is set when there is a network error, a timeout or abort, an HTTP
|
|
38
|
+
* status outside 200-299 (`HttpError`), or the body cannot be parsed.
|
|
39
|
+
*
|
|
40
|
+
* @template T - Expected type of the parsed body.
|
|
41
|
+
* @example
|
|
42
|
+
* const [error, post] = await useFetchError<Post>("/api/posts/1");
|
|
43
|
+
* if (error) return console.error(error);
|
|
44
|
+
* console.log(post.title);
|
|
45
|
+
*/
|
|
46
|
+
export async function useFetchError<T = unknown>(
|
|
47
|
+
input: RequestInfo | URL,
|
|
48
|
+
options: UseFetchErrorOptions = {}
|
|
49
|
+
): Promise<Result<T, Error>> {
|
|
50
|
+
const { parseAs = "json", timeout, ...init } = options;
|
|
51
|
+
|
|
52
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
53
|
+
let timedOut = false;
|
|
54
|
+
let onAbort: (() => void) | undefined;
|
|
55
|
+
|
|
56
|
+
if (timeout !== undefined) {
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const userSignal = init.signal;
|
|
59
|
+
if (userSignal) {
|
|
60
|
+
if (userSignal.aborted) controller.abort(userSignal.reason);
|
|
61
|
+
onAbort = () => controller.abort(userSignal.reason);
|
|
62
|
+
userSignal.addEventListener("abort", onAbort, { once: true });
|
|
63
|
+
}
|
|
64
|
+
timer = setTimeout(() => {
|
|
65
|
+
timedOut = true;
|
|
66
|
+
controller.abort();
|
|
67
|
+
}, timeout);
|
|
68
|
+
init.signal = controller.signal;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const [fetchError, response] = await to(fetch(input, init));
|
|
73
|
+
if (fetchError || !response) {
|
|
74
|
+
return [timedOut ? new TimeoutError(timeout as number) : toError(fetchError), undefined];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
return [new HttpError(response, await readErrorBody(response)), undefined];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const [parseError, data] = await to(readBody(response, parseAs));
|
|
82
|
+
if (parseError) {
|
|
83
|
+
return [timedOut ? new TimeoutError(timeout as number) : toError(parseError), undefined];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return [null, data as T];
|
|
87
|
+
} finally {
|
|
88
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
89
|
+
if (onAbort) options.signal?.removeEventListener("abort", onAbort);
|
|
90
|
+
}
|
|
91
|
+
}
|
package/src/index.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
const symbolResult = Symbol("result");
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Extends the `Function` prototype to include a method
|
|
5
|
-
* that handles errors and returns a tuple `[error, result]`.
|
|
6
|
-
*
|
|
7
|
-
* @param {...any[]} args - The arguments to be passed to the function.
|
|
8
|
-
* @returns {[any, any]} A tuple with `null` and the result if the function executes successfully,
|
|
9
|
-
* or the error and `undefined` if an exception occurs.
|
|
10
|
-
*/
|
|
11
|
-
Function.prototype[symbolResult] = function (...args) {
|
|
12
|
-
try {
|
|
13
|
-
const result = this.apply(this, args);
|
|
14
|
-
return [null, result];
|
|
15
|
-
} catch (error) {
|
|
16
|
-
return [error, undefined];
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Extends the `Promise` prototype to include a method
|
|
22
|
-
* that handles promise errors and returns a tuple `[error, result]`.
|
|
23
|
-
*
|
|
24
|
-
* @returns {Promise<[any, any]>} A promise that resolves with a tuple containing `null` and the result
|
|
25
|
-
* if the promise resolves successfully, or the error and `undefined` if it is rejected.
|
|
26
|
-
*/
|
|
27
|
-
Promise.prototype[symbolResult] = async function () {
|
|
28
|
-
try {
|
|
29
|
-
const result = await this;
|
|
30
|
-
return [null, result];
|
|
31
|
-
} catch (error) {
|
|
32
|
-
return [error, undefined];
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Makes an HTTP request using `fetch` and handles both network errors and JSON parsing errors.
|
|
38
|
-
*
|
|
39
|
-
* @param {string} url - The URL to which the request will be made.
|
|
40
|
-
* @param {RequestInit} [options={}] - Optional options to configure the `fetch` request.
|
|
41
|
-
* @returns {Promise<[any, any]>} A promise that resolves with a tuple containing the error and `undefined` if an error occurs,
|
|
42
|
-
* or `null` and the parsed data if the request and JSON parsing are successful.
|
|
43
|
-
*/
|
|
44
|
-
async function useFetchError(url, options = {}) {
|
|
45
|
-
const [error, response] = await fetch(url, options)[symbolResult]();
|
|
46
|
-
|
|
47
|
-
if (error) {
|
|
48
|
-
return [error, undefined]; // Returns the error if one occurred
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const [parseError, data] = await response.json()[symbolResult]();
|
|
52
|
-
|
|
53
|
-
if (parseError) {
|
|
54
|
-
return [parseError, undefined]; // Returns the error when parsing JSON
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return [null, data]; // Returns the result if everything went well
|
|
58
|
-
}
|