Home | Send Feedback | Share on Bluesky |

Ky - elegant fetch

Published: 10. August 2019  •  Updated: 6. January 2025  •  javascript

In this article, we are going to take a closer look at Ky, a JavaScript HTTP client library for the browser built on top of the Fetch API. Ky adds convenience functions and new features to the existing Fetch API feature set.

Ky targets modern browsers and depends on the Fetch API. If you want to use Ky on older browsers, add the Fetch polyfill. Ky also runs on Node.js (>= 18), Bun, and Deno.

You add Ky to an npm-managed project with npm install ky.

In the following article, I'll provide examples of Ky's most important features and convenience functions.

Basics

Let's look at a fetch example that sends a GET request to a server for a text response.

  let response = await fetch('http://localhost:8080/simple-get');
  let body = await response.text();

main.js

If we do the same with Ky, we will write the following code:

  response = await ky('http://localhost:8080/simple-get');
  body = await response.text();

main.js

ky accepts the same parameters as fetch. The first parameter is either a URL string or a Request object, and the second parameter is an object containing custom settings. The return value in both cases is a Promise that resolves to a Response object.

This doesn't look very exciting; it's almost the same code; we only replaced fetch with ky. However, under the hood, there are a few changes.

We will look at all these changes in more detail later in this blog post.


Both, fetch() and ky() return a Promise that resolves to a Response object. To extract the body, an application has to call one of these methods: text(), json(), formData(), blob(), and arrayBuffer(). With fetch(), these methods run asynchronously and return a Promise, so we have to wait twice: first for the response and then for the body extraction method.

Ky simplifies this process by exposing the following body methods through the Response Promise. With this improvement, we can rewrite the example from above in one line and only have to wait once.

body = await ky.get('http://localhost:8080/simple-get').text();

This is not only syntactic sugar; it also has a practical side effect. Ky automatically sets the proper Accept header in the request.

Note that these methods overwrite a custom Accept header. The following example sends Accept: */* instead of the header we configured.

body = await ky.get('http://localhost:8080/simple-get', { headers: { Accept: 'application/octet-stream' } }).arrayBuffer();

To send a custom Accept request header, you must use the long form and wait twice.

  response = await ky.get('http://localhost:8080/simple-get', { headers: { Accept: 'application/octet-stream' } });
  body = await response.arrayBuffer();

main.js

Method shortcuts

When using the Fetch API to send a request other than GET, you must specify the method option.

fetch('....', { method: 'POST', ...});
fetch('....', { method: 'PUT', ...});

Ky supports this as well.

ky('....', { method: 'POST', ...});
ky('....', { method: 'PUT', ...});

It's a bit cumbersome to always specify the method this way, so Ky introduced shortcut methods to make this more convenient.

ky.get(input, [options])
ky.post(input, [options])
ky.put(input, [options])
ky.patch(input, [options])
ky.head(input, [options])
ky.delete(input, [options])

All these methods assign the proper value to the method option ('GET', 'POST', 'PUT', 'PATCH', 'HEAD', 'DELETE').

Notice that ky.get(...) and ky(...) are equivalent because 'GET' is the default value for the method option.

TypeScript

The examples in this blog post are written in JavaScript. However, you can use generics when your code is written in TypeScript. The ky() method, all methods mentioned above (ky.get(), ...) and the json() method accept a generic parameter.

const user = await ky<User>('/api/users/2').json();
// or
const user = await ky('/api/users/3').json<User>();

The two calls are the same. In both cases, the constant user is of type User.

If you don't specify a generic type parameter, Ky defaults to unknown.

const user = await ky('/api/users/1').json();

user is of type unknown.

Posting data

Posting JSON data with fetch requires us to write a few lines of code. We have to specify the Content-Type header, the method option, and convert the object we want to send to a JSON string. If we expect a JSON response, we must check if the response is okay and extract the response body.

  let response = await fetch('http://localhost:8080/simple-post', {
    method: 'POST',
    body: JSON.stringify({ value: "hello world" }),
    headers: {
      'content-type': 'application/json'
    }
  });

  if (response.ok) {
    const body = await response.json();
    console.log(body);
  }

main.js

With Ky, this becomes a one-liner. Thanks to the post() shortcut method, we can omit the method option. With the json option, we can directly pass the object without converting it to a JSON string first. Ky internally converts the object with JSON.stringify and assigns the string to the body option. Ky also implicitly sets the Content-Type header to application/json. Lastly, we can take advantage of the exposed json() method of the Response Promise to wait on the response and the body extraction method simultaneously.

  const body = await ky.post('http://localhost:8080/simple-post', { json: { value: "hello world" } }).json();
  console.log(body);

main.js


Sending form data is identical to the Fetch API. Create a FormData object and assign it to the body option. Unlike fetch, Ky automatically sets the Content-Type header to multipart/form-data.

  const formData = new FormData();
  formData.append('value1', '10');
  formData.append('value2', 'ten');

  await ky.post('http://localhost:8080/multipart-post', {
    body: formData
  });

main.js

If you want to send a request with the Content-Type: application/x-www-form-urlencoded header, create a URLSearchParams object and assign that to the body option.

  const searchParams = new URLSearchParams();
  searchParams.set('value1', '10');
  searchParams.set('value2', 'ten');

  await ky.post('http://localhost:8080/formurlencoded-post', {
    body: searchParams
  });

main.js

Download Progress

Ky allows your application to install a download progress event handler that is regularly called when Ky fetches a resource and informs your application about the progress (percent, transferred bytes).

  await ky.get('http://localhost:8080/download', {
    onDownloadProgress: (progress, chunk) => {
      console.log(`${progress.percent * 100}% - ${progress.transferredBytes} of ${progress.totalBytes} bytes`);
    }
  });

main.js

The event handler receives a progress and chunk argument. Before Ky starts the download, it calls your event handler with progress.percent === 0 and progress.transferredBytes === 0. In this very first call, the chunk parameter is an empty Uint8Array. In subsequent calls, chunk holds a Uint8Array with the transferred bytes since the last onDownloadProgress call, not the full download.

The progress object contains the following elements: percent, transferredBytes, and totalBytes. If the server does not send a proper Content-Length header, it's not possible for ky to figure out the total length; in that case, totalBytes will be 0.

Error Handling

One significant difference to the Fetch API is that Ky throws an exception (HTTPError) if the response status code is not between 200 and 299.

The URL /notfound in this demo application returns a 404 status code. Fetch treats this as a normal response, not throwing an exception but setting the response.ok property to false. Fetch only throws exceptions when the request fails because of a network issue (e.g., server down, domain invalid).

  let response = await fetch('http://localhost:8080/notfound');
  console.log("response.ok = ", response.ok);

main.js

On the other hand, Ky throws an HTTPError exception not only for network issues but also for each response where the status code is not between 200 and 299.

  try {
    response = await ky.get('http://localhost:8080/notfound');
  } catch (e) {
    console.log('ky.get', e);
  }

main.js

You can turn off this feature by setting the throwHttpErrors option to false. Ky then behaves like the Fetch API and only throws exceptions for network issues.

  response = await ky.get('http://localhost:8080/notfound', { throwHttpErrors: false });
  console.log("response.ok = ", response.ok);

main.js

Retry

Ky's built-in retry feature is enabled by default. It resends a failed request two times. If the third attempt fails, Ky throws an exception. Note that retries are not triggered following a timeout.

You can change the number of retry attempts with the retry option.

  response = await ky.get('http://localhost:8080/retry', { retry: 5 }).text();

main.js

You can turn off the feature by setting the option to 0.

    const response = await ky.get('http://localhost:8080/retry-test', { retry: 0 }).text();

main.js

You can pass an object with more options instead of a number to fine-tune the retry behavior. If you pass an object, you specify the number of retry attempts with the limit option.

  response = await ky.get('http://localhost:8080/retry', {
    retry: {
      limit: 10,
      methods: ['get'],
      afterStatusCodes: [429]
    }
  }).text();

main.js

Ky, by default, only retries requests in case the server responds with one of these status codes: 408, 413, 429, 500, 502, 503, or 504. You can override this behavior with the statusCodes option.

Also, it only retries requests for the get, put, head, delete, options, and trace methods. You can change this with the methods option.

The next option in the retry config object is afterStatusCodes with the default codes 413, 429, and 503. If ky receives a response with one of these response codes, it will check if the response contains a Retry-After header and wait the specified amount of time before retrying the request. If the Retry-After header is missing, Ky checks for the non-standard RateLimit-Reset header. If maxRetryAfter is set and the Retry-After header is greater than maxRetryAfter, then ky uses maxRetryAfter as the wait time. If both headers are missing, Ky falls back to the normal retry behavior.

The normal retry behavior is configured with backoffLimit and delay. The defaults are undefined for backoffLimit and attemptCount => 0.3 * (2 ** (attemptCount - 1)) * 1000 for delay. The backoffLimit option is the upper limit of the delay per retry in milliseconds.

The delay option is a function that calculates the delay for each retry. The function receives the attempt count as a parameter and returns the delay in milliseconds.

Here are the wait times for the first few attempts with the default delay function:

// after 1st attempt --> wait 0.3 seconds
// after 2nd attempt --> wait 0.6 seconds
// after 3rd attempt --> wait 1.2 seconds
// after 4th attempt --> wait 2.4 seconds
// ....

Timeout

Timeout is another built-in feature of Ky. This feature, like the previous retry feature, is enabled by default, and every ky call will throw a TimeoutError exception if the server does not respond for 10 seconds. You can change the timeout with the timeout option, which expects the timeout in milliseconds.

  console.log('ky, timeout 1s');
  try {
    console.log('request ', Date.now());
    response = await ky.get('http://localhost:8080/timeout', { timeout: 1000 });
  } catch (e) {
    console.log('response', Date.now());
    console.log(e);
  }

main.js

If you want to turn off the timeout and wait as long as possible for a response, assign false to the timeout option. By turning off the timeout, you get the native Fetch API's behavior.

  response = await ky.get('http://localhost:8080/timeout', { timeout: false });

main.js

Abort

Aborting a request is a feature of the Fetch API. You can use this feature with Ky the same way you can with fetch. Create an AbortController, pass the controller.signal as an option to the request call and abort the request with controller.abort().

  const controller = new AbortController();

  setTimeout(() => {
    console.log('abort', Date.now());
    controller.abort();
  }, 2500);

  try {
    console.log('request', Date.now());
    const body = await ky.get('http://localhost:8080/timeout', { signal: controller.signal }).text();
  } catch (error) {
    console.log('exception', Date.now());
    if (error.name === 'AbortError') {
      console.log('Fetch aborted');
    } else {
      console.error('Fetch error:', error);
    }
  }

main.js

Hooks

With hooks, another feature of Ky, you can inject methods into the request/response lifecycle. Ky supports before request and after response hooks.

To configure hooks, you create an object with the beforeRequest and afterResponse properties and then assign it to the option hooks. Both properties expect an array of functions. As parameters, the beforeRequest hooks receive the request and options objects, and the afterResponse hooks receive the request instance, options, and response objects. Return values from beforeRequest hooks will be ignored. When an afterResponse hook returns an object of type Response, Ky overwrites the original response. Return values of other types will be ignored.

In the example below, we add one additional header to the request before Ky sends it to the server, and after the response comes back, we create a new response object and overwrite the response from the server.

  const hooks = {
    beforeRequest: [
      (request, options) => {
        console.log('before request');
        options.headers.set('x-api-key', '1111');
      }
    ],
    afterResponse: [
      (request, options, response) => {
        console.log('after response');
        console.log(response);
        // return different response
        return new Response('{"value": "something else"}', { status: 200 });
      }
    ]
  };


  const body = await ky.get('http://localhost:8080/simple-get', { hooks }).json();

main.js

Additionally, to these two hooks, Ky supports two more hooks: beforeRetry and beforeError. The beforeRetry hook is called before Ky retries a request. This hook lets you modify the request right before retrying or prevent Ky from retrying the request by throwing an error. Alternatively, you can return the ky.stop symbol to halt retries without throwing an error.

The beforeError hook is called before Ky throws an error. The hook receives an HTTPError instance as a parameter and should return an instance of HTTPError.

Custom default instances

Another configuration option we have yet to discuss is prefixUrl. With this option, you can specify a base URL that will be prepended to the input URL.

Without prefixUrl

const body = await ky.get('http://localhost:8080/simple-get').text();

With prefixUrl

  let body = await ky.get('simple-get', { prefixUrl: 'http://localhost:8080' }).text();

main.js

The trailing slash is optional; Ky adds it automatically if needed.


This feature does not look very useful, but combined with another Ky feature, it becomes convenient. Ky allows you to create new Ky instances that override default options. For this purpose, Ky provides the ky.extend(defaultOptions) and ky.create(defaultOptions) methods.

Somewhere at the start of your application, you create a custom ky instance

  const customKy = ky.create({ prefixUrl: 'http://localhost:8080' });

main.js

In the rest of your application, you use this instance to send requests without repeating the base URL.

  body = await customKy('simple-get').text();

main.js


extend() inherits the defaults from its parent, while create() creates an instance with completely new default values.

In the example below, we extend the custom Ky instance from above and add a custom request header. This results in a Ky instance where both options, prefixUrl and headers, are set.

  const customApiKy = customKy.extend({ headers: { 'x-api-key': '1111' } });
  body = await customApiKy('simple-get').text();

main.js


This concludes this tutorial about Ky, an elegant HTTP client built on top of the Fetch API that adds useful features and simplifies everyday tasks.

Check out the project page for more information: https://github.com/sindresorhus/ky

You can find the source code for all the examples in this blog post on GitHub: https://github.com/ralscha/blog2019/tree/master/ky