mion uses RpcError to handle all errors in routes and middleFns. The key principle is: return errors instead of throwing them to maintain full type safety on the client.
Always return errors instead of throwing them. Returned errors are part of the function signature, giving the client full type information about possible error types and their data:
import {RpcError} from '@mionkit/core';
import {Route, route} from '@mionkit/router';
import type {Pet} from './full-example.app.ts';
import {myApp} from './full-example.app.ts';
export const getPet = route(async (ctx, id: string): Promise<Pet | RpcError<'pet-not-found'>> => {
try {
const pet = await myApp.db.getPet(id);
if (!pet) {
const publicMessage = `Pet with id ${id} can't be found`;
// application errors should be returned and strongly typed,
// so can be correctly managed by client
return new RpcError({publicMessage, type: 'pet-not-found'});
}
return pet;
} catch (dbError) {
const publicMessage = `Cant fetch data.`;
const message = (dbError as Error).message;
/*
* Thrown or Unexpected error are not strongly typed
*
* Full RpcError containing dbError message and stacktrace will be added
* to ctx.request.unexpectedErrors, so it can be logged or managed after
*
* only publicMessage will be returned in the response
*/
throw new RpcError({publicMessage, message, originalError: dbError as Error, type: 'db-error'});
}
}) satisfies Route;
export const alwaysError = route((): void => {
throw new Error('will generate a 500 error with an "Unknown Error" message');
}) satisfies Route;
The client can now handle errors with full type safety:
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);
}
}
error.type to Identify Errorserror.type to identify and handle errors.In RPC-style APIs, the HTTP status code is just a transport detail. The error.type property is the discriminator that identifies the error:
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 : {...}}
}
Thrown errors are treated as unexpected errors and only expose publicMessage to the client. The error type is not part of the function signature:
import {RpcError} from '@mionkit/core';
import {Route, route} from '@mionkit/router';
import type {Pet} from './full-example.app.ts';
import {myApp} from './full-example.app.ts';
export const getPet = route(async (ctx, id: string): Promise<Pet | RpcError<'pet-not-found'>> => {
try {
const pet = await myApp.db.getPet(id);
if (!pet) {
const publicMessage = `Pet with id ${id} can't be found`;
// application errors should be returned and strongly typed,
// so can be correctly managed by client
return new RpcError({publicMessage, type: 'pet-not-found'});
}
return pet;
} catch (dbError) {
const publicMessage = `Cant fetch data.`;
const message = (dbError as Error).message;
/*
* Thrown or Unexpected error are not strongly typed
*
* Full RpcError containing dbError message and stacktrace will be added
* to ctx.request.unexpectedErrors, so it can be logged or managed after
*
* only publicMessage will be returned in the response
*/
throw new RpcError({publicMessage, message, originalError: dbError as Error, type: 'db-error'});
}
}) satisfies Route;
export const alwaysError = route((): void => {
throw new Error('will generate a 500 error with an "Unknown Error" message');
}) satisfies Route;
Returned and thrown errors are serialized differently in the response body:
@thrownErrors field{
"getUser": {
"type": "user-not-found",
"publicMessage": "User not found",
"errorData": {"userId": "123"}
},
"@thrownErrors": {
"someMiddleFn": {
"type": "unknown-error",
"publicMessage": "Unexpected error"
}
}
}
error or errors from the result object. The internal message property is never sent to the client.Routes and middleFns can return multiple error types. The client receives a discriminated union of all possible errors:
import {RpcError} from '@mionkit/core';
import {Route, route} from '@mionkit/router';
import type {Pet} from './full-example.app.ts';
import {myApp} from './full-example.app.ts';
export const getPet = route(async (ctx, id: string): Promise<Pet | RpcError<'pet-not-found'>> => {
try {
const pet = await myApp.db.getPet(id);
if (!pet) {
const publicMessage = `Pet with id ${id} can't be found`;
// application errors should be returned and strongly typed,
// so can be correctly managed by client
return new RpcError({publicMessage, type: 'pet-not-found'});
}
return pet;
} catch (dbError) {
const publicMessage = `Cant fetch data.`;
const message = (dbError as Error).message;
/*
* Thrown or Unexpected error are not strongly typed
*
* Full RpcError containing dbError message and stacktrace will be added
* to ctx.request.unexpectedErrors, so it can be logged or managed after
*
* only publicMessage will be returned in the response
*/
throw new RpcError({publicMessage, message, originalError: dbError as Error, type: 'db-error'});
}
}) satisfies Route;
export const alwaysError = route((): void => {
throw new Error('will generate a 500 error with an "Unknown Error" message');
}) satisfies Route;
MiddleFns with runOnError: true execute even when previous middleFns or routes return/throw errors. Useful for logging or cleanup:
import {CallContext, middleFn, Routes} from '@mionkit/router';
import {myApp} from './full-example.app.ts';
const routes = {
// using the middleFn function to define a middleFn
logger: middleFn(
async (ctx: CallContext): Promise<void> => {
const hasErrors = ctx.request.thrownErrors && Object.keys(ctx.request.thrownErrors).length > 0;
if (hasErrors) await myApp.cloudLogs.error(ctx.path, ctx.request.thrownErrors);
else myApp.cloudLogs.log(ctx.path, ctx.shared.me.name);
},
// ensures logger is executed even if there are errors in the route or other middleFns
{runOnError: true}
),
// ... other routes and middleFns
} satisfies Routes;
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);
}
}