Client

Error Handling

Learn how to handle errors in the mion client using the Result pattern.

The Result Pattern

mion client uses a Result Pattern for responses, that returns a tuple with 4 elements:

[routeResult, routeError, middleFnsResults, middleFnsErrors]
  • 0: Route result (if successful)
  • 1: Route error (if failed)
  • 2: MiddleFn results (by middleFn name)
  • 3: MiddleFn errors (by middleFn name)

The API calls never throw, this provides type-safe error handling with full TypeScript support!

The result pattern was chosen as provides better support for multiple SubRequests responses on a single call.

Basic Usage

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

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

async function basicErrorHandling() {
    // call() returns 4-tuple - never throws
    const [user, error] = await routes.users.getById('123').call();

    if (error) {
        // Handle error - TypeScript knows the error type
        console.log('Error:', error.publicMessage);
    } else {
        // Handle success
        console.log('User:', user?.name);
    }
}

Handling Typed Errors

When routes define specific error types, TypeScript provides full autocompletion and type checking:

import {initClient} from '@mionkit/client';

// importing type only from server
import type {MyApi} from './server.routes.ts';
import {isRpcError} from '@mionkit/core';

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

// ========== Result pattern (never throws) ==========
// call() and callWithMiddleFns() always return a 4-tuple, never throw
// [routeResult, routeError, middleFnsResults, middleFnsErrors]

// calls sayHello route in the server
const [sayHello, error] = await routes.users.sayHello({id: '123', name: 'John', surname: 'Doe'}).call();

if (error) {
    // in this case the request has failed because the authorization middleFn is missing
    console.log(error); // { type: 'validation-error', message: `Invalid params for Route or MiddleFn 'auth'.`}

    if (isRpcError(error)) {
        // ... handle the error as required
    }
} else {
    console.log(sayHello); // Hello John Doe
}

// ========== Full 4-tuple with middleFns ==========
// callWithMiddleFns() returns [routeResult, routeError, middleFnsResults, middleFnsErrors]
const [greeting, routeError, middleFnResults, middleFnErrors] = await routes.users
    .sayHello({id: '123', name: 'John', surname: 'Doe'})
    .callWithMiddleFns({
        auth: middleFns.auth({headers: {Authorization: 'Bearer token'}}),
    });

if (routeError) {
    console.log('Route failed:', routeError.type);
} else {
    console.log(greeting); // Hello John Doe
}

// Check middleFn errors
if (middleFnErrors?.auth) {
    console.log('Auth middleFn failed:', middleFnErrors.auth.type);
}

// Access middleFn results
console.log('MiddleFn results:', middleFnResults);

// ========== Validation throws errors ==========
// Note: typeErrors() is the only method that can throw

try {
    // Validation throws an error when validation fails
    const errors = await routes.users.sayHello(null as any).typeErrors();
    console.log(errors); // [] (empty array if no errors)
} catch (validationError: any) {
    console.log(validationError); // { type: 'validation-error', message: `Invalid params ...`, errorData : {...}}
}

Using callWithMiddleFns

When you need to pass middleFn data to a single route call, use callWithMiddleFns(). This method returns a tuple that includes both route and middleFn results.

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

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

// calls route with auth middleFn
// Returns 4-tuple: [routeResult, routeError, middleFnsResults, middleFnsErrors]
const [user, routeError, middleFnResults, middleFnErrors] = await routes.users.getById('123').callWithMiddleFns({
    auth: middleFns.auth(new HeadersSubset({Authorization: 'myToken-XYZ'})),
});

if (routeError || middleFnErrors?.auth) {
    console.log('Something failed');
} else {
    console.log(user); // User object
}

Prefilling MiddleFns

For middleFns that should be included in all requests (like authentication), use prefill():

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

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

declare function redirectToLogin(): void;
declare function refreshToken(): Promise<string>;

// prefill() returns a TypedEvent for registering persistent handlers
// TypedEvent handlers are STRONGLY TYPED by the error.type string
middleFns
    .auth(new HeadersSubset({Authorization: 'myToken-XYZ'}))
    .prefill()
    .onSuccess((session) => {
        // Called after every successful auth
        console.log('Authenticated as:', session?.userId);
    })
    .onError('not-authorized', (error) => {
        // TypeScript knows error.type is 'invalid-token'
        console.log('Auth failed:', error.publicMessage);
        redirectToLogin();
    });

// call() returns a 4-tuple with middleFnResults and middleFnErrors
// These are NOT strongly typed - they contain generic RpcError types
const [sum, error, middleFnResults, middleFnErrors] = await routes.utils.sum(5, 2).call();

// Both TypedEvent handlers AND 4-tuple receive the same middleFn data:
// - TypedEvent handlers were already called above (if auth succeeded/failed)
// - middleFnResults/middleFnErrors also contain the auth result/error
if (middleFnErrors?.auth) {
    console.log('Auth error (generic type):', middleFnErrors.auth.publicMessage);
}
if (middleFnResults?.auth) {
    console.log('Session from tuple:', middleFnResults.auth);
}
if (!error) {
    console.log(sum); // 7
}
Please note that prefill().onError().onSuccess() handlers are called on every request regardless if they are handled in the route call response or not.

TypedEvent for Prefilled MiddleFns

When you prefill a middleFn, you can register persistent success/error handlers using TypedEvent:

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

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

declare function redirectToLogin(): void;
declare function refreshToken(): Promise<string>;

// prefill() returns a TypedEvent for registering persistent handlers
// TypedEvent handlers are STRONGLY TYPED by the error.type string
middleFns
    .auth(new HeadersSubset({Authorization: 'myToken-XYZ'}))
    .prefill()
    .onSuccess((session) => {
        // Called after every successful auth
        console.log('Authenticated as:', session?.userId);
    })
    .onError('not-authorized', (error) => {
        // TypeScript knows error.type is 'invalid-token'
        console.log('Auth failed:', error.publicMessage);
        redirectToLogin();
    });

// call() returns a 4-tuple with middleFnResults and middleFnErrors
// These are NOT strongly typed - they contain generic RpcError types
const [sum, error, middleFnResults, middleFnErrors] = await routes.utils.sum(5, 2).call();

// Both TypedEvent handlers AND 4-tuple receive the same middleFn data:
// - TypedEvent handlers were already called above (if auth succeeded/failed)
// - middleFnResults/middleFnErrors also contain the auth result/error
if (middleFnErrors?.auth) {
    console.log('Auth error (generic type):', middleFnErrors.auth.publicMessage);
}
if (middleFnResults?.auth) {
    console.log('Session from tuple:', middleFnResults.auth);
}
if (!error) {
    console.log(sum); // 7
}

Prefill vs callWithMiddleFns

Aspect.prefill().callWithMiddleFns()
PersistenceMiddleFn data stored for ALL future requestsMiddleFn data sent with this request only
Error HandlersPersistent via .onError()Check middleFnErrors in tuple
Use CaseAuth tokens, session dataMetadata related to the request

Complete Example

import {initClient} from '@mionkit/client';
// importing only the RemoteApi type from server
import type {MyApi} from './server.routes.ts';
import {HeadersSubset} from '@mionkit/core';

const john = {id: '123', name: 'John', surname: 'Doe'};
const {routes, middleFns} = initClient<MyApi>({baseURL: 'http://localhost:3000'});

async function fullExample() {
    // prefills auth token for any future requests, value is stored in localStorage by default
    await middleFns.auth(new HeadersSubset({Authorization: 'myToken-XYZ'})).prefill();

    // calls sayHello route in the server - call() returns [result, error] tuple
    const [greeting, error] = await routes.users.sayHello(john).call();
    if (error) {
        console.log('Error:', error.publicMessage);
    } else {
        console.log(greeting); // Hello John Doe
    }

    // validate parameters locally without calling the server
    const validationErrors = await routes.users.sayHello(john).typeErrors();
    console.log(validationErrors); // [] (empty array if no errors)
}

Type Reference

Result

// Error processing code-import:
// Start comment marker not found: // type-result-start

CallWithMiddleFnsResult

// Error processing code-import:
// Start comment marker not found: // type-call-with-middleFns-result-start

TypedEvent

// Error processing code-import:
// Start comment marker not found: // type-typed-event-start

RpcError

export class RpcError<ErrType extends string, ErrData = any>
    extends TypedError<ErrType>
    implements RpcErrorParams<ErrType, ErrData>
{
    // Note: name is NOT declared as a property here
    // It is inherited from Error class and assigned in constructor
    // This prevents it from being included in type reflection for JIT validation
    /**
     * id of the error, ideally each error should unique identifiable
     * * if RouterOptions.autoGenerateErrorId is set to true and id with timestamp+uuid will be generated
     * */
    public readonly id?: number | string;
    /** the message that will be returned in the response */
    public readonly publicMessage: string;
    /** options data related to the error, ie validation data, must be json serializable */
    public readonly errorData?: Readonly<ErrData>;
    /** optional http status code */
    statusCode?: number;

    constructor({message, publicMessage, originalError, errorData, type, id, statusCode}: AnyErrorParams<ErrType, ErrData>) {
        const originalMessage = message || originalError?.message || publicMessage || '';

        // Call parent TypedError constructor
        super({
            message: originalMessage,
            originalError,
            type,
        });

        const {autoGenerateErrorId} = options;
        this.id = id ?? (autoGenerateErrorId ? randomUUID_V7() : undefined);
        this.publicMessage = publicMessage || '';
        this.errorData = errorData;
        this.statusCode = statusCode;

        // Override name to be non-enumerable
        Object.defineProperty(this, 'name', {
            value: 'RpcError',
            writable: true,
            enumerable: false,
            configurable: true,
        });

        Object.setPrototypeOf(this, RpcError.prototype);
    }
}