Client

Cancellation & Timeouts

Cancel in-flight requests and enforce timeouts using AbortSignal.

Per-request Cancellation

Use a standard AbortController to cancel a specific request. Pass the signal via the call() setup object. Aborted requests return an error with type === 'request-aborted'.

import {initClient} from '@mionjs/client';
import type {MyApi} from './server.routes.ts';

const {routes} = initClient<MyApi>({baseURL: 'http://localhost:3000'});

// create an AbortController for this request
const controller = new AbortController();

// pass the signal via call setup
const resultPromise = routes.users.sayHello({id: '1', name: 'John', surname: 'Doe'}).call({signal: controller.signal});

// cancel the request (e.g. on component unmount or user action)
controller.abort();

const [result, error] = await resultPromise;
// error.type === 'request-aborted'

Per-request Timeout

Set a timeout in milliseconds to automatically cancel a request if it takes too long. Timed-out requests return an error with type === 'request-timeout'.

import {initClient} from '@mionjs/client';
import type {MyApi} from './server.routes.ts';

const {routes} = initClient<MyApi>({baseURL: 'http://localhost:3000'});

// this request will fail if it takes longer than 5 seconds
const [result, error] = await routes.users.sayHello({id: '1', name: 'John', surname: 'Doe'}).call({timeout: 5000});

if (error?.type === 'request-timeout') {
    console.log('Request took too long');
}

Client-level Default Timeout

Set a default timeout for all requests via initClient. Per-request timeouts override the client default.

import {initClient} from '@mionjs/client';
import type {MyApi} from './server.routes.ts';

// all requests timeout after 10 seconds unless overridden per-request
const {routes} = initClient<MyApi>({
    baseURL: 'http://localhost:3000',
    timeout: 10_000,
});

// uses the 10s default timeout
const [r1] = await routes.users.sayHello({id: '1', name: 'John', surname: 'Doe'}).call();

// overrides to 2s for this specific call
const [r2, err] = await routes.users.sayHello({id: '1', name: 'John', surname: 'Doe'}).call({timeout: 2000});

Global Abort

Use client.abort() to cancel all in-flight requests at once -- useful for SPA page navigation. New requests work normally after calling abort().

import {initClient} from '@mionjs/client';
import type {MyApi} from './server.routes.ts';

const {client, routes} = initClient<MyApi>({baseURL: 'http://localhost:3000'});

// start multiple requests
const p1 = routes.users.sayHello({id: '1', name: 'John', surname: 'Doe'}).call();
const p2 = routes.utils.sum(5, 2).call();

// cancel ALL in-flight requests (e.g. user navigated away)
client.abort();

// both return 'request-aborted' errors
const [, err1] = await p1; // err1.type === 'request-aborted'
const [, err2] = await p2; // err2.type === 'request-aborted'

// new requests work normally after abort
const [greeting] = await routes.users.sayHello({id: '1', name: 'John', surname: 'Doe'}).call();
client.destroy() also aborts all in-flight requests before clearing handlers.

Combined with MiddleFns and RoutesFlow

Cancellation and timeouts work seamlessly with middleFns and routesFlow. Pass signal and timeout alongside other setup options.

import {initClient, routesFlow} from '@mionjs/client';
import type {MyApi} from './server.routes.ts';
import {HeadersSubset} from '@mionjs/core';

const {routes, middleFns} = initClient<MyApi>({baseURL: 'http://localhost:3000'});

const controller = new AbortController();

// cancellation works with middleFns
const [result, error, mfResults, mfErrors] = await routes.users.sayHello({id: '1', name: 'John', surname: 'Doe'}).call({
    middleFns: {auth: middleFns.auth(new HeadersSubset({Authorization: 'myToken-XYZ'}))},
    timeout: 5000,
    signal: controller.signal,
});

// and with routesFlow
const [[sum, greeting], [sumError, greetingError]] = await routesFlow([
    routes.utils.sum(5, 2),
    routes.users.sayHello({id: '1', name: 'John', surname: 'Doe'}),
]).call({
    middleFns: {auth: middleFns.auth(new HeadersSubset({Authorization: 'myToken-XYZ'}))},
    timeout: 10_000,
});
When using routesFlow with a timeout, the timeout applies to the entire batch request, not to individual routes within it.

Aborting Mutations: What Actually Happens

Aborting an in-flight HTTP request stops the client from waiting — it does not undo work on the server. By the time the abort signal fires, the server may have:

  • Not yet received the request — safe, no side effects
  • Received and started processing — partially committed
  • Fully processed and committed the result before the client noticed
Aborted mutations should be treated as unknown, not as failed. The server may have changed state. There is no client-side way to tell the difference.

mion's client.abort(), per-request signal, and timeout options are best-effort: they cleanly cancel the client-side wait, but the server-side outcome is the same as if the connection had simply dropped. Server-side cancellation propagation is a separate concern not currently implemented.

Patterns to handle this safely

  • Idempotency keys — generate a unique key per logical action and include it in the request (e.g. as a header or parameter). The server stores the key with the result; retries with the same key return the cached result. This makes "abort + retry" safe and is the pattern used by Stripe, Square, GitHub, and most production payment/mutation APIs.
  • Refetch & reconcile — after an abort, refetch the affected resource to determine its actual state. Show "syncing…" rather than "failed".
  • Don't expose abort for critical mutations — only allow cancellation for safe-to-retry operations. Let payments and similar irrevocable actions run to completion.
  • Treat request-aborted differently in your UI — don't show an error toast for user-initiated cancels; ignore them silently or show a neutral indicator.

Queries vs mutations

Cancellation is unconditionally safe for queries (GET routes defined with query()): the worst case is wasted work on the server. For mutations (mutation() and default route() calls), apply the patterns above and design with the assumption that any aborted request might have committed.