@alwatr/fetch
v7.1.0
Published
`@alwatr/fetch` is an enhanced, lightweight, and dependency-free wrapper for the native `fetch` API. It provides modern features like caching strategies, request retries, timeouts, and intelligent duplicate request handling, all in a compact package.
Maintainers
Readme
@alwatr/fetch

@alwatr/fetch is an enhanced, lightweight, and dependency-free wrapper for the native fetch API. It provides modern features like caching strategies, request retries, timeouts, and intelligent duplicate request handling, all in a compact package.
It's designed to be a drop-in replacement for the standard fetch to instantly upgrade your application's network layer.
Key Features
- Go-Style Error Handling: Returns a tuple
[Response, null]on success or[null, FetchError]on failure—no exceptions thrown. - Retry Pattern: Automatically retries failed requests on timeouts or server errors (5xx).
- Request Timeout: Aborts requests that take too long to complete.
- Duplicate Handling: Prevents sending identical parallel requests, returning a single response for all callers.
- Caching Strategies: Leverages the browser's Cache API with strategies like
stale_while_revalidate. - Simplified API: Send JSON and URL parameters with ease using
bodyJsonandqueryParams. - TypeScript First: Written entirely in TypeScript for a great developer experience.
Installation
Install the package using your preferred package manager:
# npm
npm i @alwatr/fetch
# yarn
yarn add @alwatr/fetch
# pnpm
pnpm add @alwatr/fetchQuick Start
Import the fetch function and use it with tuple destructuring for elegant error handling. The function returns [Response, null] on success or [null, FetchError] on failure—no exceptions are thrown.
import {fetch} from '@alwatr/fetch';
async function fetchProducts() {
console.log('Fetching product list...');
const [response, error] = await fetch('/api/products', {
queryParams: {limit: 10, category: 'electronics'},
cacheStrategy: 'stale_while_revalidate',
timeout: '5s',
});
if (error) {
console.error('Failed to fetch products:', error.message);
console.error('Error reason:', error.reason);
return;
}
// At this point, response is guaranteed to be valid and ok
const data = await response.json();
console.log('Products:', data);
}
fetchProducts();Error Handling
@alwatr/fetch uses a Go-style tuple return pattern instead of throwing exceptions. This provides explicit, type-safe error handling.
Return Type
type FetchResponse = Promise<[Response, null] | [null, FetchError]>;- Success:
[Response, null]- The response is guaranteed to haveresponse.ok === true - Failure:
[null, FetchError]- Contains detailed information about what went wrong
FetchError Class
All errors are returned as FetchError instances, which provide rich context about the failure:
class FetchError extends Error {
reason: FetchErrorReason; // Specific error reason
response?: Response; // The HTTP response (if available)
data?: unknown; // Parsed response body (if available)
}Error Reasons
The reason property indicates why the request failed:
'http_error': HTTP error status (e.g., 404, 500)'timeout': Request exceeded the timeout duration'cache_not_found': Resource not found in cache (when usingcache_only)'network_error': Network-level error (e.g., DNS failure, connection refused)'aborted': Request was aborted via AbortSignal'unknown_error': Unspecified error
Error Handling Example
const [response, error] = await fetch('/api/user/profile', {
bearerToken: 'jwt-token',
});
if (error) {
switch (error.reason) {
case 'http_error':
console.error(`HTTP ${error.response?.status}:`, error.data);
break;
case 'timeout':
console.error('Request timed out. Please try again.');
break;
case 'network_error':
console.error('Network error. Check your connection.');
break;
case 'cache_not_found':
console.error('Data not available offline.');
break;
default:
console.error('Request failed:', error.message);
}
return;
}
// Safe to use response here
const userData = await response.json();API and Options
The fetch function takes a url string and an options object. The options object extends the standard RequestInit and adds several custom options for enhanced control.
| Option | Type | Default | Description |
| :------------------- | :---------------------------------------------- | :--------------- | :--------------------------------------------------------------------------------------------- |
| method | HttpMethod | 'GET' | The HTTP request method. |
| headers | HttpRequestHeaders | {} | An object representing the request's headers. |
| timeout | Duration | 8_000 (8s) | Request timeout in milliseconds or as a duration string (e.g., '5s'). Set to 0 to disable. |
| retry | number | 3 | Number of retries if the request fails with a server error (5xx) or times out. |
| retryDelay | Duration | 1_000 (1s) | Delay between retry attempts in milliseconds or as a duration string. |
| removeDuplicate | 'never' \| 'always' \| 'until_load' \| 'auto' | 'never' | Strategy for handling identical parallel requests. body is included for uniqueness. |
| cacheStrategy | 'network_only' \| 'network_first' \| ... | 'network_only' | Caching strategy using the browser's Cache API. |
| cacheStorageName | string | 'fetch_cache' | Custom name for the CacheStorage instance. |
| revalidateCallback | (response: Response) => void | undefined | Callback executed with the new response when using stale_while_revalidate strategy. |
| bodyJson | Json | undefined | A JavaScript object sent as the request body. Sets Content-Type to application/json. |
| queryParams | Dictionary | undefined | An object of query parameters appended to the URL. |
| bearerToken | string | undefined | A bearer token added to the Authorization header. |
| alwatrAuth | {userId: string; userToken: string} | undefined | Alwatr-specific authentication credentials. |
... and all other standard RequestInit properties like signal, credentials, etc.
Features in Detail
Query Parameters
The queryParams option simplifies adding search parameters to your request URL.
// This will make a GET request to: /api/users?page=2&sort=asc
const [response, error] = await fetch('/api/users', {
queryParams: {page: 2, sort: 'asc'},
});
if (error) {
console.error('Failed to fetch users:', error.message);
return;
}
const users = await response.json();JSON Body
Use bodyJson to send a JavaScript object as a JSON payload. The Content-Type header is automatically set to application/json.
// This will make a POST request to /api/orders with a JSON body
const [response, error] = await fetch('/api/orders', {
method: 'POST',
bodyJson: {
productId: 'xyz-123',
quantity: 2,
},
});
if (error) {
console.error('Failed to create order:', error.message);
return;
}
const order = await response.json();
console.log('Order created:', order);Timeout
Set a timeout for your requests. If the request takes longer than the specified duration, it will be aborted and return a FetchError with reason: 'timeout'.
const [response, error] = await fetch('/api/slow-endpoint', {
timeout: '2.5s', // You can use duration strings
});
if (error) {
if (error.reason === 'timeout') {
console.error('Request timed out after 2.5 seconds');
}
return;
}Retry Pattern
The fetch operation will automatically retry on server errors (5xx status codes) or timeouts.
// Retry up to 5 times, with a 2-second delay between each attempt
const [response, error] = await fetch('/api/flaky-service', {
retry: 5,
retryDelay: '2s',
});
if (error) {
console.error('Request failed after 5 retries:', error.message);
return;
}
const data = await response.json();Duplicate Request Handling
The removeDuplicate option prevents multiple identical requests from being sent simultaneously. The uniqueness of a request is determined by its method, URL, and body.
'never'(default): Does nothing.'until_load': Caches thePromiseof a request until it resolves. Subsequent identical requests will receive a clone of the first response.'always': Caches the response indefinitely (for the lifetime of the application).'auto': Uses'until_load'if the Cache API is available, otherwise'always'.
// Both calls will result in only ONE network request.
// The second call will receive the response from the first.
const results = await Promise.all([
fetch('/api/data', {removeDuplicate: 'until_load'}),
fetch('/api/data', {removeDuplicate: 'until_load'}),
]);
// Both results will have the same response or error
const [response1, error1] = results[0];
const [response2, error2] = results[1];Cache Strategies
Leverage the browser's Cache API with cacheStrategy.
'network_only'(default): Standard fetch behavior; no caching.'cache_first': Serves from cache if available. Otherwise, fetches from the network and caches the result.'network_first': Fetches from the network first. If the network fails, it falls back to the cache.'cache_only': Only serves from cache; returns an error if not found.'update_cache': Fetches from network and updates the cache.'stale_while_revalidate': The fastest strategy. It serves stale content from the cache immediately while sending a network request in the background to update the cache for the next time.
// Serve news from cache instantly, but update it in the background for the next visit.
const [response, error] = await fetch('/api/news', {
cacheStrategy: 'stale_while_revalidate',
revalidateCallback: (freshResponse) => {
console.log('Cache updated with fresh data!');
// You can use freshResponse to update the UI if needed
},
});
if (error) {
console.error('Failed to load news:', error.message);
return;
}
const news = await response.json();Authentication
Easily add authentication headers with bearerToken or the alwatrAuth scheme.
// Using a Bearer Token
const [response, error] = await fetch('/api/secure/data', {
bearerToken: 'your-jwt-token-here',
});
if (error) {
if (error.response?.status === 401) {
console.error('Authentication failed. Please log in again.');
}
return;
}
const data = await response.json();
// Using Alwatr's authentication scheme
const [response2, error2] = await fetch('/api/secure/data', {
alwatrAuth: {
userId: 'user-id',
userToken: 'user-auth-token',
},
});Sponsors
The following companies, organizations, and individuals support Nanolib's ongoing maintenance and development. Become a Sponsor to get your logo on our README and website.
Contributing
Contributions are welcome! Please read our contribution guidelines before submitting a pull request.
