liaisev5.3.1

Compare

How liaise compares

A harness in the liaise repo runs axios, ky, ofetch and liaise through 10 failure scenarios against a local server, and records what the calling code gets back. Each library runs every scenario twice: set up the way its own docs describe, and out of the box. The harness also measures bundle size and request overhead, with plain fetch as the baseline. How it compares has the rules that keep it fair.

Measured on in Node 22.18.0 on darwin arm64, against axios 1.20.0, ky 2.1.0, ofetch 1.5.1 and liaise 5.3.0. Plain fetch, the baseline, is Node's own.

This run measured liaise 5.3.0; the current release is 5.3.1.

Other libraries change, so this is a dated snapshot. Every scenario runs in Node. In browsers axios uses XHR (its adapters), so its results there can differ.

Rerun it from the repo root:

npm run build && cd compare && npm install && npm run compare

If you maintain one of these libraries and think its setup is unfair, a pull request is welcome: each library's setup is one file in compare/contenders.

The scenarios

  • no throwThe call didn't throw. The text says what happened.
  • throwsThe call threw.
  • your codeNeeded hand-written code beyond the library's docs.
  • no optionThe library has no built-in option for this.

Select a cell to see how that library was set up, with links to the docs each option comes from. A scenario's name links to the page on how liaise handles it. Every note and the code behind it: compare/results.md.

With each library's documented setup. Only options and patterns from each library's own docs. Code beyond the docs, where a scenario can't be done otherwise, is marked.

  1. Server answers 500

    • axios
    • ky
    • ofetch
    • liaise
  2. Server unreachable

    • axios
    • ky
    • ofetch
    • liaise
  3. Server never answers

    • axios
    • ky
    • ofetch
    • liaise
  4. 200 with broken JSON

    • axios
    • ky
    • ofetch
    • liaise
  5. 204 with no body, on a JSON call

    • axios
    • ky
    • ofetch
    • liaise
  6. Search as you type: which results stay on screen

    • axios
    • ky
    • ofetch
    • liaise
  7. Five requests get a 401 at once

    • axios
    • ky
    • ofetch
    • liaise
  8. Slow 503s, 3 s deadline, 3 retries: when does the caller hear back

    • axios
    • ky
    • ofetch
    • liaise
  9. Path param is undefined

    • axios
    • ky
    • ofetch
    • liaise
  10. Response is missing a field the type promises

    • axios
    • ky
    • ofetch
    • liaise

Cells that needed hand-written code: axios 3, ofetch 3, ky 2 and liaise 1.

liaise needed it only in “Five requests get a 401 at once”. No liaise cell throws.

liaise is the only one that refuses an undefined path param before sending the request.

Milliseconds are shown only where timing is the point.

Size

One JSON GET per library, as a minified ES2020 ESM bundle for a browser, gzip and brotli compressed. For liaise that bundle holds the REST client only; GraphQL, polling and the middleware are separate imports, and Exports gives the size of each.

Plain fetch is the baseline, not a library: it is built into the runtime, so its row is the call site only.

Librarygzip (kB)brotli (kB)
plain fetch (baseline)0.10.1
axios19.117.3
ky9.68.5
ofetch4.03.6
liaise7.06.4
liaise + retryMiddleware7.66.9

Gzipped or brotli-compressed, liaise is larger than ofetch and smaller than ky and axios.

Requests per second

Median (min–max) of 10 interleaved rounds of 2,000 calls each, after 2,000 warm-up calls per library. Measured on localhost, keep-alive as each library defaults; a real network adds milliseconds per request, this measures microseconds. Differences under about 5% are noise.

Plain fetch is the baseline: what each library adds to a request is measured against it.

LibraryOne at a time50 in flight
plain fetch (baseline)16,550 (14,668–17,117)19,496 (17,587–20,150)
axios13,540 (11,196–13,872)15,556 (14,322–16,090)
ky13,721 (12,672–14,694)15,895 (15,217–16,246)
ofetch16,157 (15,632–16,639)18,840 (18,412–19,610)
liaise16,131 (15,574–16,791)18,797 (17,854–19,341)

One request at a time, liaise ties plain fetch and ofetch; ky handles about 15% fewer requests per second than liaise, axios about 16% fewer.

With 50 requests in flight, liaise ties plain fetch and ofetch; ky handles about 15% fewer requests per second than liaise, axios about 17% fewer.

Where liaise loses

  • No default timeout. Out of the box, a liaise call to a server that never answers is still waiting after 15s. Only ky times out by default. Set timeout and the call ends with “error result (timeout)”, as in the documented setup.
  • A 204 on a JSON call is a parse error. Out of the box, liaise gives “error result (parse)” for a 204 with no body on a JSON call; ky throws; axios and ofetch resolve. An endpoint that answers 204 needs responseType: 'none'.
  • One endpoint, JSON or an empty body. liaise has no option for an endpoint that answers JSON or an empty body, as the harness notes. In ky's documented setup, parseJson handles an empty body.
  • No retries out of the box. With no options set, ky made 3 and ofetch 2 attempts at the slow 503s; liaise made 1. liaise retries with retryMiddleware.
  • Larger than ofetch. Gzipped, liaise is 7.0 kB; ofetch 4.0 kB.

Closest alternative: ky

Apart from throwing where liaise returns an error result, ky's documented setup differs from liaise's in 2 of the 10 rows, fewer than any other library here:

Scenariokyliaise
Search as you type: which results stay on screenshows "rea"your codeshows "rea"no throw
Path param is undefinedrequests /s/users/undefinedno throwrefused before sending: error result (network)no throw

Out of the box, only ky times out, and ky and ofetch retry.

ky is 9.6 kB gzipped, liaise 7.0 kB.

Pick something else when…

  • You need a normalized GraphQL cache, optimistic updates or subscriptions. Use Apollo Client or urql. Apollo's caching overview and urql's Graphcache docs explain how each one caches.
  • You want failed calls to throw. ky throws an HTTPError for a response outside 2xx unless you turn throwHttpErrors off.
  • You want caching and refetching in your UI. Use TanStack Query, with liaise underneath: a query function can be any function that returns a promise. The TanStack Query recipe shows the two together.
  • You make two or three calls. Plain fetch is fine.

When it fits, and when it doesn't has the longer version.

esc