Guide
Cancelling, deadlines and stale requests
This page covers three ways to end a call early: your own signal, a deadline, and dedupe. Read it when a user can leave a page mid-request, when a server can hang, or when fast typing starts a call on every keystroke.
Each one ends the call with an error you can tell apart. A signal or dedupe gives kind: 'abort', and a deadline gives kind: 'timeout'.
Cancel with a signal
Pass an AbortSignal in the call options:
import { createApi, defineRequest } from 'liaise'
type User = { id: string; name: string }
const getUser = defineRequest<User>()({ method: 'GET', path: '/users/:id' })
const api = createApi({ baseUrl: 'https://api.example.com', requests: { getUser } })
const controller = new AbortController()
const pending = api.getUser({ id: '42' }, { signal: controller.signal })
controller.abort()
const { error } = await pending
// error.kind === 'abort', error.status === 0, error.body is a DOMException named 'AbortError'
A cancellation you asked for isn’t reported to onError. How liaise tells your cancellation apart from other failures is under Abort classification.
Set a deadline with timeout
timeout is in milliseconds, and there is no default: without one, a call waits as long as the server takes. You set it on the client, on the endpoint or on one call. Here the client gives 10 seconds to every endpoint that sets none, and getReport sets its own 3:
import { createApi, defineRequest } from 'liaise'
import { retryMiddleware } from 'liaise/middleware'
type Report = { total: number }
const getReport = defineRequest<Report>()({ method: 'GET', path: '/report', timeout: 3000 })
const api = createApi({
baseUrl: 'https://api.example.com',
requests: { getReport },
middleware: [retryMiddleware(3)],
timeout: 10_000, // for every endpoint that sets none
})
const { error } = await api.getReport()
// If no answer arrives within 3 seconds, retries included: error.kind === 'timeout', error.status === 0
The endpoint’s 3 seconds replace the client’s 10, so a server that never answers ends the call at 3 seconds with kind: 'timeout'.
timeoutis one deadline for the whole call. It covers every middleware, every retry and every wait between retries.timeout: 3000with three retries still answers within three seconds.- The most specific
timeoutwins. A call’s replaces the endpoint’s, and the endpoint’s replaces the client’s. Zero or a negative number at the winning level means no deadline, sotimeout: 0on an endpoint opts it out of the client’s, and on a call turns both off. result.retry()starts a fresh deadline. The retried call isn’t charged for time the first one used.- If you want a separate limit for each attempt instead, see Give each attempt its own timeout.
- Under
share, the endpoint’s or the client’stimeoutalso bounds the one shared request (details). - A fractional
timeoutis rounded down to whole milliseconds, with a minimum of 1 ms (CallOptions).
Drop stale calls with dedupe
dedupe: true makes each new call to an endpoint cancel the one still running. Use it for search-as-you-type and fast-changing filters, where only the latest answer matters:
type User = { id: string; name: string }
const searchUsers = defineRequest<User[], { q: string }>()({
method: 'GET',
path: '/users/search',
dedupe: true,
})
const api = createApi({ baseUrl: 'https://api.example.com', requests: { searchUsers } })
// Typing fast: each call cancels the one before it.
api.searchUsers({ q: 'h' }) // ends with kind 'abort'
api.searchUsers({ q: 'he' }) // ends with kind 'abort'
api.searchUsers({ q: 'hel' }) // this one completes
- It works per endpoint. A call to one endpoint never cancels a call to another.
- Copies from
withHeaders()that add the same headers dedupe together, and copies with different headers never cancel each other’s calls.{ dedupe: false }turns it off for a copy. - A replaced call ends with
kind: 'abort', so your code can ignore it. - It works together with your own signal and a
timeout. Whichever fires first ends the call. - It can’t be combined with
share, which does the opposite.
Search as you type builds a search box on dedupe, and shows that an older search can’t land after a newer one.
When a middleware is stuck
All three still end the call when a middleware is stuck on work of its own that ignores the signal, such as a token refresh that never settles. Timeout backstop explains how.
Next: Sharing identical requests does the opposite of dedupe: it joins the call already running instead of cancelling it.