Go-style error handling for JavaScript that never throws. tryFetch returns [error, data, response] tuples, tryCatch returns [error, result] tuples.
npm install try-fetch-catch

Go-style error handling for JavaScript/TypeScript.
try-fetch-catch provides two small utilities that replace thrown exceptions with predictable tuples:
- tryFetch wraps fetch() and returns [error, data, response].
- tryCatch wraps any sync/async function and returns [error, result].
No thrown exceptions. No nested try/catch. No unhandled promise rejections. Just linear, easy-to-read code.
``bash`
npm install try-fetch-catch
Requirements:
- Node.js 18+ (native fetch)
- Or any runtime that provides the Fetch API (modern browsers, Deno, Bun)
Import in your project:
`ts`
import { tryFetch, tryCatch } from "try-fetch-catch";
// OR
const { tryFetch, tryCatch } = require("try-fetch-catch");
> Note: This package ships native ESM and CommonJS builds.
- π― Clean code - Linear, no-throw control flow without nested try/catch boilerplate.
- π Drop-in fetch replacement - Replace fetch with tryFetch and access err + data on the same line.Response
- β‘ Smart auto-parsing β Parses by the Content-Type header automatically, with easy overrides or a custom parser when you need full control.
- π« No uncaught exceptions β Thrown exceptions are captured internally and returned as typed error values.
- πͺ TypeScript-friendly - Generics and exported types for predictable, typed results.
- π¦ Tiny & dependency-free - Minimal footprint, zero dependencies, low maintenance.
1. Fetching Data
`ts
// BEFORE (Traditional fetch)
try {
const response = await fetch("/api/users/123");
if (!response.ok) throw new Error("HTTP Error");
const data = await response.json();
} catch (err) {
console.error(err);
}
// AFTER (tryFetch)
const [err, data] = await tryFetch("/api/users/123");
if (err) console.error(err);
`
> Key advantages: Linear, automatic HTTP & parse handling, fully predictable.
2. Parsing Data
`ts
// BEFORE (Traditional JSON.parse)
try {
const user = JSON.parse(userDataString);
} catch (err) {
console.error(err);
}
// AFTER (tryCatch)
const [err, user] = tryCatch(JSON.parse, userDataString);
if (err) console.error(err);
`
> Key advantages: Works for sync/async, linear flow, no try/catch boilerplate.
tryFetch is a drop-in replacement for native fetch that never throws.
Instead of relying on thrown exceptions, it always returns a tuple where errors, data, and the response are explicit and predictable.
> Core rule: tryFetch never throws. All failures are returned as data.
Use tryFetch exactly like fetch, but destructure the result instead of wrapping it in try/catch.
`ts
import { tryFetch } from "try-fetch-catch";
const [err, user] = await tryFetch);
if (err) {
// handle error
return;
}
// user is safe and already parsed`
renderUser(user);
This pattern keeps control flow linear and makes error handling impossible to forget - a common source of bugs in the JavaScript world.
tryFetch always resolves to a tuple with one of the following shapes:
- Success
[null, data, Response][HttpError, data | null, Response]
- HTTP error (non-2xx response)
(data is null if the response body canβt be parsed)
- Network / transport error (no response)
[NetworkError, null, undefined]
- Response parse error
[ParseError, null, Response]
(request succeeded, but body parsing failed)
A common error handling pattern:
`ts/api/users/${id}
const [err, data, res] = await tryFetch);
if (err?.status === 0) {
// Network error: no data and HTTP response
return showOfflineUI(err.message);
}
if (err) {
// HTTP error (4xx / 5xx)
return showErrorPage(err.status);
}
// Success - data is safe
renderUser(data);
`
> Best practice:
> Always check err before using data.
`ts`
tryFetch
input: RequestInfo | URL,
init?: RequestInit & { tryFetchOptions?: TryFetchOptions
): Promise
By default, tryFetch automatically parses the response body based on the Content-Type header:
- JSON (application/json, +json) β parsed JSONtext/\*
- Text (, XML types) β response.text()image/\*
- Binary (, application/octet-stream, application/pdf) β response.blob()multipart/form-data
- Form data (, application/x-www-form-urlencoded) β response.formData()JSON.parse
- Unknown or missing type β reads text, then attempts a best-effort content-length: 0
- Empty bodies (204/205, , or empty JSON) β null
In most cases, you never need to think about parsing β the returned data is already usable.
> Note: On HTTP error responses, tryFetch still attempts to parse the response body so APIs can return structured error payloads.
Auto-parsing covers most APIs. When you need more control, you have two options.
#### Force a specific parse mode
`ts`
const [err, text] = await tryFetch
tryFetchOptions: { parseAs: "text" },
});
Accepted values for parseAs:
"json" | "text" | "blob" | "arrayBuffer" | "formData"
#### Provide a custom parser
`ts`
const [err, upper] = await tryFetch
tryFetchOptions: {
parser: async (response) => (await response.text()).toUpperCase(),
},
});
If a custom parser throws, the error is returned as a ParseError.
By default, tryFetch consumes the response body once while parsing it.
- Network error returns no Response.Response
- HTTP responses (success or error) return a whose body is already consumed.status
- You can still safely access metadata like , statusText, and headers.
If you need to read the response body more than once, enable preserveResponse:
`ts
const [err, data, res] = await tryFetch("/api/data", {
tryFetchOptions: { preserveResponse: true },
});
if (!err) {
const again = await res.json(); // body is still readable
}
`
When preserveResponse is enabled, parsing is performed on a cloned Response so the returned Response remains readable.
tryFetchOptions is passed inside the native fetch init object:
`ts`
await tryFetch(url, {
...RequestInit,
tryFetchOptions: {
/ ... /
},
});
| Option | Type | Accepted values | Default | Usage |
| ------------------ | ----------------------------------------- | ------------------------------------------------------------------- | ------------------------------ | ---------------------------------------------- |
| parseAs | ResponseParseAs | "json" \| "text" \| "blob" \| "arrayBuffer" \| "formData" | Auto-detect via Content-Type | tryFetchOptions: { parseAs: "text" } |parser
| | (response: Response) => T \| Promise | Any function | none | tryFetchOptions: { parser: (r) => r.text() } |preserveResponse
| | boolean | true / false | false | tryFetchOptions: { preserveResponse: true } |
Notes:
- parser overrides parseAs and Content-Type auto-parsing.preserveResponse: true
- ensures the returned Response remains readable.
tryCatch wraps any synchronous or asynchronous function and returns [error, result] instead of throwing.
It always resolves to a predictable tuple:
- Success: [null, result][Error, null]
- Failure:
> Non-Error throws are coerced to Error type.
Asynchronous function:
`ts
// Direct function + arguments
const [err, user] = await tryCatch(loadUser, userId);
// OR using a callback
const [err2, user2] = await tryCatch(() => loadUser(userId));
if (err) return log.error(err.message);
log.info(user);
`
Synchronous function:
`ts
// Direct function + arguments
const [err, user] = tryCatch(JSON.parse, userDataString);
// OR using a callback
const [err2, user2] = tryCatch(() => JSON.parse(userDataString));
if (err) return;
log.info(user);
`
This package exports a small set of TypeScript types for tryFetch.
`ts`
import type {
FetchResult,
HttpError,
NetworkError,
ParseError,
TryFetchOptions,
TryFetchRequestInit,
ResponseParseAs,
} from "try-fetch-catch";
tryFetch always resolves to one of these tuple shapes:
- Success: [null, T, Response][HttpError, T | null, Response]
- HTTP error (non-2xx): [ParseError, null, Response]
- Parse error (2xx but body parsing failed): [NetworkError, null, undefined]
- Network/transport error (no Response):
Use TryFetchOptions under init.tryFetchOptions to force parsing (parseAs) or provide a custom parser.
Convenience type for the init parameter: native RequestInit plus tryFetchOptions.
tryCatch is intentionally minimal: it returns [err, result].
- Success: [null, T][Error, null]
- Failure:
Note: result can still be null on success if the wrapped function can return null (i.e. your T includes null`).
Sion Young
ISC