ky throws on non-2xx and drops your base URL when you pass a Request
🌳 Tiny & elegant JavaScript HTTP client based on the Fetch API
At a glance
- What is it?
- ky is Sindre Sorhus's dependency-free HTTP client built on the Fetch API, aimed at browsers, Node, Bun and Deno. It throws an HTTPError on any non-2xx status, adds body shortcuts that decide the Accept header, validates responses through Standard Schema, and its readme on the default branch explicitly documents the next version.
- Who is it for?
- ky fits a codebase that already targets fetch, in a browser or a modern runtime, and that wants retries, timeouts, hooks and a JSON body option without adopting a client with its own interceptor model. It does not fit a codebase that treats a non-2xx response as data, because the body shortcuts throw, and a 204 from a delete is a thrown error unless you pass the custom JSON parsing option.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 36 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.
Star history
- sindresorhus/ky17,163 ★ · +83
| Repository | 2026-09-21 | 2026-11-13 |
|---|---|---|
| sindresorhus/ky | 17.1k | 17.2k |
83 Stars in the last 53 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
A 500 is a rejected promise, not a Response you forgot to check
The single biggest behavioural difference from fetch is on the benefits list and it is worth restating. Ky treats non-2xx status codes as errors, after redirects, and the added body methods throw an HTTPError when the status is not in the 200 to 299 range, unlike the body methods on a fetch response. The file demonstrates the gap by writing out the plain fetch version of the same call, and that version needs a custom error class, a check of the ok property, the content type header set by hand and the body stringified by hand, so five steps become one. The consequence cuts both ways. A whole class of bug disappears, the one where a caller forgets to check ok and ships an error page body as data. And a new class appears, because a status you expected has become something you have to catch, so every call site that previously inspected a status code now needs a try block.
The readme in the repository documents the next major, not the package
The install section carries a note that is easy to skim past and important if you are evaluating versions. It says this readme is for the next version of Ky, and links to the current version on the package registry instead. The manifest in the repository is at 2.2.0, the newest release tag is v2.2.0 from 2026-10-09, and the last push recorded is 2026-10-09. So the documentation on the default branch is deliberately ahead of what the registry holds, which is a common pattern for a library that keeps its next-major readme on main. The cost is that a reader comparing the file in front of them with the package they just installed will find differences and have no way to tell from either side whether the difference is a bug or a version skew. Check the branch and the installed version together before you conclude anything about the API.
Passing a Request ignores your base URL and your prefix
The input accepts the same three things fetch accepts, a string, a URL or a Request, and the options are the same plus extras. The extras include a base URL option and instances with custom defaults, which is where a lot of ky's appeal lives. Then there is this: when you pass a Request instance as the input, the prefix and baseUrl options are ignored. The searchParams option still applies, so the rest of the options layer continues to work and these two specifically do not. That is a silent failure with a specific shape, because a request factory that builds a Request and passes it to a configured instance will resolve against no base at all, and the resulting request either 404s or goes somewhere you did not intend with no warning from the library. It is compounded by the other rule in the same section, which is that a Content-Type on a Request input is replaced, because the Request constructor sets one automatically from its body, so your content type is discarded even when you set it deliberately.
undefined deletes a query parameter and null survives as the string null
The searchParams option accepts a string, an object, an array of pairs or a URLSearchParams, and it merges with whatever the input URL already carries. The rule inside that is the one to memorise. When you pass an object, setting a value to undefined deletes the parameter, including one that was already in the input URL, and the deletion still applies even when a later option layer adds the parameter back. That is usually what you want when you build parameters from a configuration object. The README gives null values separate handling, so check that rule before using null to mean not provided.
Which body method you call decides the Accept header that goes out
The return value is a Response object with the body methods added for convenience, which is why you can call ky.get(input).json() without awaiting the response first, and the file says an appropriate Accept header will be set depending on the body method used. That is a small design decision with a visible consequence: the same URL fetched with the text shortcut and fetched with the JSON shortcut sends different Accept headers, and a content-negotiating server may answer differently. It also means Accept is not an independent setting unless you override the headers yourself. The shortcuts are json, text, formData, arrayBuffer, blob and bytes, and the last one is the exception, since the file says it is only present when the runtime supports the matching method on Response, so the API surface is not identical across the environments this package targets.
An empty body is an error, and parseJson is the per-call escape hatch
Two related behaviours sit on the JSON path and both are worth knowing before you wire this into an existing API client. First, the JSON shortcut throws if the body is empty, because there is nothing to parse, and the file tells you to use the parseJson option to add custom handling for empty bodies. That means a 204 response, which is the correct answer to a successful delete, arrives as a thrown error rather than as an empty object unless you use the parseJson option. Second, you can pass a Standard Schema compatible validator to the same call, for example a Zod schema at version 3.24 or later, and validation failure throws a dedicated error carrying its issues. The upside is that a response is checked at the point you read it rather than by a separate layer, and the cost is a dependency on a schema ecosystem and one more failure mode to catch.
No dependencies, a Node 22 floor, and a compiled distribution
Installing it is one command, and there is a CDN route for each of three hosts if you would rather not install:
npm install kyThe manifest is as spare as the claim. The package is ESM only, side effects are declared false so bundlers can drop it, the exports and the main and types fields all point into a distribution directory, and the published files list contains that directory alone, which is compiled output rather than source. The build is a TypeScript compile into it, and a prepare script runs that build, so installing straight from a git checkout compiles the package. The one floor in the manifest is a Node version of 22 or newer, which is worth noting next to the readme's claim that the library targets modern browsers, Node, Bun and Deno, because a bundler or an install tool will read that engine field and may warn on a Node version the library would run on elsewhere. The development dependencies are where the real work shows, with a browser test driver, a memory leak detector and a type-level test helper, and the test script lints, builds, type-checks and then runs.
Editorial conclusion
ky fits a codebase that already targets fetch, in a browser or a modern runtime, and that wants retries, timeouts, hooks and a JSON body option without adopting a client with its own interceptor model. It does not fit a codebase that treats a non-2xx response as data, because the body shortcuts throw, and a 204 from a delete is a thrown error unless you pass the custom JSON parsing option. Before you adopt it, read the readme on the branch you are actually on, since the one in the repository says outright that it documents the next version while the newest release tag is v2.2.0. Then check the two silent behaviours, because they are the ones that cost time: passing a Request ignores your base URL and prefix, and a Content-Type on that Request is replaced. And if you need raw bytes, remember the bytes shortcut only exists where the runtime supports the matching method on Response, so the API surface is not identical in every environment.
Frequently asked questions
Which is better, axios or ky?
What the project documentation sets out is the difference between ky and plain fetch, shown by writing out the fetch version of the same call, which needs a custom error class, a content type header, a manual stringify and an ok check. Axios appears among the package keywords next to got and node-fetch.
Why does the ky readme describe a version that is not published?
It says so itself, in a note in the install section: the readme is for the next version of Ky, with a link to the current version on the package registry. The manifest in the repository is at 2.2.0, which matches the newest release tag.
What happens when you pass a Request object to ky?
The prefix and baseUrl options are ignored, while the searchParams option still applies. A Content-Type set on the Request is also replaced, because the Request constructor sets one automatically from its body.
How do I get a file or raw bytes with ky?
The added body shortcuts are json, text, formData, arrayBuffer, blob and bytes, so a file arrives through arrayBuffer or blob. The bytes shortcut only exists when the runtime supports the matching method on Response, which is why the API surface differs between environments. Upload and download progress are listed among the benefits over fetch.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sindresorhus-ky)Star history chart for your README
[](https://hysenlabs.com/projects/sindresorhus-ky)Was this useful?