Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve error typings #78

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 106 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ To address these challenges, the `fetchf()` provides several enhancements:
1. **Consistent Error Handling:**

- In JavaScript, the native `fetch()` function does not reject the Promise for HTTP error statuses such as 404 (Not Found) or 500 (Internal Server Error). Instead, `fetch()` resolves the Promise with a `Response` object, where the `ok` property indicates the success of the request. If the request encounters a network error or fails due to other issues (e.g., server downtime), `fetch()` will reject the Promise.
- This approach aligns error handling with common practices and makes it easier to manage errors consistently.
- The `fetchff` plugin aligns error handling with common practices and makes it easier to manage errors consistently by rejecting erroneous status codes.

2. **Enhanced Retry Mechanism:**

Expand All @@ -127,7 +127,7 @@ To address these challenges, the `fetchf()` provides several enhancements:

3. **Improved Error Visibility:**

- **Error Wrapping:** The `createApiFetcher()` and `fetchf()` wrap errors in a custom `RequestError` class, which provides detailed information about the request and response, similarly to what Axios does. This makes debugging easier and improves visibility into what went wrong.
- **Error Wrapping:** The `createApiFetcher()` and `fetchf()` wrap errors in a custom `ResponseError` class, which provides detailed information about the request and response. This makes debugging easier and improves visibility into what went wrong.

4. **Extended settings:**
- Check Settings table for more information about all settings.
Expand Down Expand Up @@ -412,34 +412,74 @@ The following options are available for configuring interceptors in the `Request
<br>
Error handling strategies define how to manage errors that occur during requests. You can configure the <b>strategy</b> option to specify what should happen when an error occurs. This affects whether promises are rejected, if errors are handled silently, or if default responses are provided. You can also combine it with <b>onError</b> interceptor for more tailored approach.

### Example
<br>
<br>

The native `fetch()` API function doesn't throw exceptions for HTTP errors like `404` or `500` — it only rejects the promise if there is a network-level error (e.g., the request fails due to a DNS error, no internet connection, or CORS issues). The `fetchf()` function brings consistency and lets you align the behavior depending on chosen strategy. By default, all errors are rejected.

### Configuration

#### `strategy`

**`reject`**: (default)
Promises are rejected, and global error handling is triggered. You must use `try/catch` blocks to handle errors.

```typescript
try {
const { data } = await fetchf('https://api.example.com/', {
strategy: 'reject', // It is default so it does not really needs to be specified
});
} catch (error) {
console.error(error.status, error.statusText, error.response, error.config);
}
```

Here's an example of how to configure error handling:
**`softFail`**:
Returns a response object with additional property of `error` when an error occurs and does not throw any error. This approach helps you to handle error information directly within the response's `error` object without the need for `try/catch` blocks.

```typescript
const { data, error } = await fetchf('https://api.example.com/', {
strategy: 'reject', // Use 'reject' strategy for error handling (default)
strategy: 'softFail',
});

if (error) {
console.error(error.status, error.statusText, error.response, error.config);
}
```

### Configuration
Check `Response Object` section below to see how `error` object is structured.

The `strategy` option can be configured with the following values:
_Default:_ `reject`.
**`defaultResponse`**:
Returns a default response specified in case of an error. The promise will not be rejected. This can be used in conjunction with `flattenResponse` and `defaultResponse: {}` to provide sensible defaults.

- **`reject`**:
Promises are rejected, and global error handling is triggered. You must use `try/catch` blocks to handle errors.
```typescript
const { data, error } = await fetchf('https://api.example.com/', {
strategy: 'defaultResponse',
defaultResponse: {},
});

- **`softFail`**:
Returns a response object with additional properties such as `data`, `error`, `config`, `request`, and `headers` when an error occurs. This approach avoids throwing errors, allowing you to handle error information directly within the response object without the need for `try/catch` blocks.
if (error) {
console.error('Request failed', data); // "data" will be equal to {} if there is an error
}
```

- **`defaultResponse`**:
Returns a default response specified in case of an error. The promise will not be rejected. This can be used in conjunction with `flattenResponse` and `defaultResponse: {}` to provide sensible defaults.
**`silent`**:
Hangs the promise silently on error, useful for fire-and-forget requests without the need for `try/catch`. In case of an error, the promise will never be resolved or rejected, and any code after will never be executed. This strategy is useful for dispatching requests within asynchronous wrapper functions that do not need to be awaited. It prevents excessive usage of `try/catch` or additional response data checks everywhere. It can be used in combination with `onError` to handle errors separately.

- **`silent`**:
Hangs the promise silently on error, useful for fire-and-forget requests without the need for `try/catch`. In case of an error, the promise will never be resolved or rejected, and any code after will never be executed. This strategy is useful for dispatching requests within asynchronous wrapper functions that do not need to be awaited. It prevents excessive usage of `try/catch` or additional response data checks everywhere. It can be used in combination with `onError` to handle errors separately.
```typescript
async function myLoadingProcess() {
const { data } = await fetchf('https://api.example.com/', {
strategy: 'silent',
});

### How It Works
// In case of an error nothing below will ever be executed.
console.log('This console log will not appear.');
}

myLoadingProcess();
```

##### How It Works

1. **Reject Strategy**:
When using the `reject` strategy, if an error occurs, the promise is rejected, and global error handling logic is triggered. You must use `try/catch` to handle these errors.
Expand All @@ -456,6 +496,53 @@ _Default:_ `reject`.
5. **Custom Error Handling**:
Depending on the strategy chosen, you can tailor how errors are managed, either by handling them directly within response objects, using default responses, or managing them silently.

#### `onError`

The `onError` option can be configured to intercept errors:

```typescript
const { data } = await fetchf('https://api.example.com/', {
strategy: 'softFail',
onError(error) {
// Intercept any error
console.error('Request failed', error.status, error.statusText);
},
});
```

#### Different Error and Success Responses

There might be scenarios when your successful response data structure differs from the one that is on error. In such circumstances you can use union type and assign it depending on if it's an error or not.

```typescript
interface SuccessResponseData {
bookId: string;
bookText: string;
}

interface ErrorResponseData {
errorCode: number;
errorText: string;
}

type ResponseData = SuccessResponseData | ErrorResponseData;

const { data, error } = await fetchf<ResponseData>('https://api.example.com/', {
strategy: 'softFail',
});

// Check for error here as 'data' is available for both successful and erroneous responses
if (error) {
const errorData = data as ErrorResponseData;

console.log('Request failed', errorData.errorCode, errorData.errorText);
} else {
const successData = data as SuccessResponseData;

console.log('Request successful', successData.bookText);
}
```

</details>

## 🗄️ Smart Cache Management
Expand Down Expand Up @@ -746,10 +833,10 @@ Each request returns the following Response Object of type <b>FetchResponse&lt;R

- **`error`**:

- **Type**: `ResponseErr`
- **Type**: `ResponseError<ResponseData, QueryParams, PathParams, RequestBody>`

- An object with details about any error that occurred or `null` otherwise.
- **`name`**: The name of the error (e.g., 'ResponseError').
- **`name`**: The name of the error, that is `ResponseError`.
- **`message`**: A descriptive message about the error.
- **`status`**: The HTTP status code of the response (e.g., 404, 500).
- **`statusText`**: The HTTP status text of the response (e.g., 'Not Found', 'Internal Server Error').
Expand Down
26 changes: 26 additions & 0 deletions docs/examples/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,10 +340,36 @@ async function example7() {
console.log('Example 7', response);
}

// fetchf() - different error payload
async function example8() {
interface SuccessResponseData {
bookId: string;
bookText: string;
}

interface ErrorResponseData {
errorCode: number;
errorText: string;
}

const { data, error } = await fetchf<SuccessResponseData | ErrorResponseData>(
'https://example.com/api/custom-endpoint',
);

if (error) {
const errorData = data as ErrorResponseData;

console.log('Example 8 Error', errorData.errorCode);
} else {
console.log('Example 8 Success', data);
}
}

example1();
example2();
example3();
example4();
example5();
example6();
example7();
example8();
41 changes: 41 additions & 0 deletions src/errors/fetch-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type {
DefaultParams,
DefaultPayload,
DefaultResponse,
DefaultUrlParams,
FetchResponse,
RequestConfig,
} from '../types';

/**
* This is a base error class
*/
export class FetchError<
ResponseData = DefaultResponse,
QueryParams = DefaultParams,
PathParams = DefaultUrlParams,
RequestBody = DefaultPayload,
> extends Error {
status: number;
statusText: string;
request: RequestConfig<ResponseData, QueryParams, PathParams, RequestBody>;
config: RequestConfig<ResponseData, QueryParams, PathParams, RequestBody>;
response: FetchResponse<ResponseData, RequestBody> | null;

constructor(
message: string,
request: RequestConfig<ResponseData, QueryParams, PathParams, RequestBody>,
response: FetchResponse<ResponseData, RequestBody> | null,
) {
super(message);

this.name = 'FetchError';

this.message = message;
this.status = response?.status || 0;
this.statusText = response?.statusText || '';
this.request = request;
this.config = request;
this.response = response;
}
}
24 changes: 24 additions & 0 deletions src/errors/network-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { FetchError } from './fetch-error';
import type {
DefaultParams,
DefaultPayload,
DefaultResponse,
DefaultUrlParams,
RequestConfig,
} from '../types';

export class NetworkError<
ResponseData = DefaultResponse,
QueryParams = DefaultParams,
PathParams = DefaultUrlParams,
RequestBody = DefaultPayload,
> extends FetchError<ResponseData, QueryParams, PathParams, RequestBody> {
constructor(
message: string,
request: RequestConfig<ResponseData, QueryParams, PathParams, RequestBody>,
) {
super(message, request, null);

this.name = 'NetworkError';
}
}
26 changes: 26 additions & 0 deletions src/errors/response-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { FetchError } from './fetch-error';
import type {
DefaultParams,
DefaultPayload,
DefaultResponse,
DefaultUrlParams,
FetchResponse,
RequestConfig,
} from '../types';

export class ResponseError<
ResponseData = DefaultResponse,
QueryParams = DefaultParams,
PathParams = DefaultUrlParams,
RequestBody = DefaultPayload,
> extends FetchError<ResponseData, QueryParams, PathParams, RequestBody> {
constructor(
message: string,
request: RequestConfig<ResponseData, QueryParams, PathParams, RequestBody>,
response: FetchResponse<ResponseData, RequestBody> | null,
) {
super(message, request, response);

this.name = 'ResponseError';
}
}
18 changes: 0 additions & 18 deletions src/request-error.ts

This file was deleted.

Loading
Loading