# Wretch clones its client on every call, which is why your headers do not leak into the next request

> Wretch is a TypeScript wrapper around fetch that collapses response parsing and error checking into one chain. Its defining choice is immutability, and its cost is an exports map with several entry points to learn.

**elbywan/wretch** — A tiny wrapper built around fetch with an intuitive syntax. :candy:

- Repository: https://github.com/elbywan/wretch
- Stars: 5,177 · Forks: 110
- Language: TypeScript
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/elbywan-wretch

## Fetch stays silent on a 404, and this wrapper refuses to

The first thing Wretch changes is control flow. Plain fetch resolves on an HTTP error status, so the status check is manual work in every call site:

```javascript
wretch("https://jsonplaceholder.typicode.com/posts/1")
  .get()
  .notFound(error => { /* … */ })
  .unauthorized(error => { /* … */ })
  .error(418, error => { /* … */ })
  .res(response => {/* … */ })
  .catch(error => { /* uncaught errors */ })
```

Wretch throws when the response is not successful, and the named handlers are the branches. `.notFound` and `.unauthorized` cover two codes you would otherwise write by hand, `.error(418, ...)` takes any code you care about, and `.catch` is where everything uncaught lands. The same chain also ends in `.res`, which is the only place the raw Response is available.

For code migrating from fetch, this is the part to audit first. A handler that used to run on `response.ok === false` now runs as an error branch, and a promise chain that relied on resolving with a 404 body will no longer do that unless you catch it. The README does not document a flag for opting back into fetch's non-throwing behaviour.

## Every call returns a clone, so shared clients stop being a shared-state bug

Immutability is the mechanism behind the ergonomics. Every call creates a cloned instance, which is what lets a configured client be declared once at module level and reused:

```javascript
const token = "MY_SECRET_TOKEN"

// Cross origin authenticated requests on an external API
const externalApi = wretch("https://jsonplaceholder.typicode.com") // Base url
  // Authorization header
  .auth(`Bearer ${token}`)
  // Cors fetch options
  .options({ credentials: "include", mode: "cors" })
  // Handle 403 errors
  .resolve((w) => w.forbidden(error => { /* Handle all 403 errors */ }));

// Fetch a resource
const resource = await externalApi
  // Add a custom header for this request
  .headers({ "If-Unmodified-Since": "Wed, 21 Oct 2015 07:28:00 GMT" })
  .get("/posts/1")
```

The token, the CORS options and the 403 handler belong to `externalApi`. The `If-Unmodified-Since` header belongs to `resource` alone, because `.headers()` returns another clone. Under a mutable client, that per-request header would sit on the shared object and ride along on the next unrelated call.

`.resolve()` is the part with a learning curve. It takes a function that receives the client and returns a configured clone, which is how per-status defaults are attached. Read it as a configuration hook rather than a promise resolver, because the promise it sounds like it resolves is not what it touches.

## The parser is the terminal method, and it takes an optional transform

The second fetch habit Wretch removes is the second callback. Fetch needs `.then(response => response.json())` before you can touch data, and the fix is to make the parser the last call in the chain:

```javascript
// Use .res for the raw response, .text for raw text, .json for json, .blob for a blob …
wretch("https://jsonplaceholder.typicode.com/posts/1")
  .get()
  .json(json => {
    // Do stuff with the parsed json
    return json
  });
```

`.json()` without an argument resolves with the parsed value. `.json(fn)` passes the value through `fn` and resolves with whatever `fn` returns, so the transform happens before your awaiting code sees the data. `.text()`, `.blob()` and `.res()` are the parallel terminals for raw text, a Blob, and the untouched Response.

The consequence is that you pick the body type at the end of the chain rather than at the start, and there is one terminal per request. A caller who needs both the parsed body and the status code has to reach for `.res`, because `.json()` consumes the body and does not hand back the Response it came from.

## Sending a JSON body is one call, with the header and method folded in

The third fetch habit is assembling a POST by hand: set `Content-Type`, set the method, stringify the body. Wretch's shorthand does all three in one call:

```javascript
wretch("https://jsonplaceholder.typicode.com/posts")
  .post({ "hello": "world" })
  .res(response => { /* … */ })
```

The object becomes the serialized body and the method becomes POST, so the call site carries only what you meant to send. This is the smallest change in the library and the one with the widest effect on a codebase: a post helper written once can be reused for every endpoint that takes JSON.

What the shorthand does not express is anything about the endpoint. Content negotiation, request identifiers and conditional headers still go through `.headers()`, and query strings still belong in the path argument. If your API needs a non-JSON body, the shorthand is not the tool, and you are back to setting the content type yourself and pairing it with `.post()`.

## The package exposes five entry points, and the browser bundle is the odd one out

package.json at version 3.0.9 is ESM with `engines.node` set to `>=22`. The entry points are not one file. `main` is `./dist/bundle/wretch.min.cjs` for require, `module` is `./dist/index.js`, `unpkg` is `./dist/bundle/wretch.min.js`, and `types` is `./dist/index.d.ts`. The `exports` map adds `.`, `./all`, `./addons`, `./addons/*` and `./middlewares`, each with separate `require` and `import` entries and their own declaration files.

That `./all` entry is the convenience build, the addons subpaths are the tree-shakeable ones, and `./middlewares` is its own namespace because middlewares are interceptors rather than request helpers. The `typesVersions` block also maps old deep paths such as `dist/*` onto the current layout, which is what keeps pre-exports TypeScript imports resolving.

The browser path is where this gets awkward. Multiple bundles sit under `/dist/bundle` by format and feature set, and if you pick the core bundle you must import addons separately from `/dist/bundle/addons/[addonName].min.js`. A script-tag user therefore ends up managing several files by hand and has to know which bundle carries which feature.

## Addons and middlewares are the extension points, and only middlewares see the request

The feature list calls the library modular: addons add features, middlewares intercept requests. Those are different jobs and the difference decides where your code goes.

An addon is what you reach for when a feature is missing, which is why the core bundle leaves them out and why the script-tag path needs separate files. A middleware is what you reach for when you want to watch or change every request the client makes, including the base URL, the auth header and any per-request headers, because those all live on the cloned instance that the middleware chain wraps.

The tree tells you the rest of the story about how the project is built. `src/` holds the source, `test/` the tests, `rolldown.config.ts` the bundler configuration that produces the `dist/bundle` variants, and `web-test-runter.config.js` the browser test runner. There are three tsconfig files, `tsconfig.json`, `tsconfig.cjs.json` and `tsconfig.type-tests.json`, which is what lets one source tree emit both the ESM and the CJS declaration paths the exports map points at.

Version 3 is the current line, with `MIGRATION_V2_V3.md` and `MIGRATION_V1_V2.md` in the repository root. Releases 3.0.7, 3.0.8 and 3.0.9 came out in March, May and June 2026, so the entry points above describe what npm serves right now.

## Conclusion

Reach for Wretch when your HTTP layer is small enough that throwing on error status and collapsing the body callback are worth more than staying on raw fetch, and when you can live with Node.js 22 as the server floor. Pass if you depend on fetch semantics that stay silent on 4xx and 5xx, because the throw is the point and cannot be switched off in the examples given. Before upgrading, read MIGRATION_V2_V3.md, and if you load the library from a script tag, remember that the core bundle does not carry addons, which have to be imported separately.

## FAQ

### What does wretch mean?

In this repository wretch is the npm package name for a fetch wrapper, expanded in the README as f[ETCH] [WR]apper. It is a TypeScript library at version 3.0.9 whose core is under 1.8KB g-zipped, not a dictionary entry or a game item.

### What is a wretch in the wretch fetch wrapper?

It is a small wrapper around fetch that parses response bodies, throws on unsuccessful responses and serializes JSON request bodies. It runs on modern browsers, Node.js 22 and newer, Deno and Bun, and is installed with npm i wretch.

### Is it wretch or retch when installing the fetch wrapper?

The package name is wretch, with a c. The README shows npm i wretch and notes that yarn or pnpm work the same way, and the import statement is written as import wretch from "wretch".

### What does it mean to wretch in terms of this library's behavior?

Nothing in the repository implements that sense of the word. What wretch does is wrap fetch so that a non-successful response throws, so that .json(), .text(), .blob() and .res() end a request chain, and so that each call returns a cloned client with its own headers and options.

## Sources

- [elbywan/wretch on GitHub](https://github.com/elbywan/wretch)
- [Issues](https://github.com/elbywan/wretch/issues)
- [License: MIT](https://github.com/elbywan/wretch/blob/master/LICENSE)
- [README](https://github.com/elbywan/wretch/blob/master/README.md)
- [Releases](https://github.com/elbywan/wretch/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/elbywan-wretch
