Client

Validation Errors

Transform validation errors into user-friendly messages for frontend applications.

When validation fails, mion returns anRpcError<'validation-error', ValidationErrorData> containing technical information about the failure. The getFriendlyErrors function transforms these errors into user-friendly messages that can be displayed directly in your UI.

Defining Friendly Error Handlers

The FriendlyErrors<T> type creates a map that mirrors your data structure. Each property can have a handler function that receives error parameters and returns a friendly message string.

import type {FriendlyErrors} from '@mionkit/core';
import {RouteParamType} from '@mionkit/client';

import type {MyApi} from './friendly-errors-server.ts';
export type User = RouteParamType<MyApi['setUser'], 0>;

export const userFriendlyErrors: FriendlyErrors<User> = {
    // Handler is called ONCE per field with ALL aggregated error params
    name: (failed) => {
        // All failed constraints are available at once
        const messages: string[] = [];
        if (failed.minLength) messages.push(`at least ${failed.minLength.val} characters`);
        if (failed.maxLength) messages.push(`at most ${failed.maxLength.val} characters`);
        if (messages.length > 0) return `Name must be ${messages.join(' and ')}`;
        return 'Name must be a valid string';
    },
    age: (failed) => {
        const messages: string[] = [];
        if (failed.min) messages.push(`at least ${failed.min.val}`);
        if (failed.max) messages.push(`at most ${failed.max.val}`);
        if (messages.length > 0) return `Age must be ${messages.join(' and ')} years`;
        return 'Age must be a valid number';
    },
    balance: (failed) => {
        if (failed.min) return `Balance cannot be negative`;
        return 'Balance must be a valid number';
    },
    isActive: () => `isActive must be a boolean (true or false)`,
    tags: (failed) => {
        return `Tag ${failed.index} must be a valid string`;
    },
    createdAt: (failed) => {
        const messages: string[] = [];
        if (failed.date) messages.push('date format is invalid');
        if (failed.time) messages.push('time format is invalid');
        if (failed.splitChar) messages.push('separator is invalid');
        if (messages.length > 0) return `Created at: ${messages.join(', ')}`;
        return `Created at must be a valid date-time string`;
    },
    nested: {
        email: (failed) => {
            const messages: string[] = [];
            if (failed.pattern) messages.push('invalid format');
            if (failed.localPart) messages.push('invalid username');
            if (failed.domain) messages.push('invalid domain');
            if (messages.length > 0) return `Email: ${messages.join(', ')}`;
            return `Email must be a valid string`;
        },
        score: (failed) => {
            const messages: string[] = [];
            if (failed.min) messages.push(`at least ${failed.min.val}`);
            if (failed.max) messages.push(`at most ${failed.max.val}`);
            if (messages.length > 0) return `Score must be ${messages.join(' and ')}`;
            return `Score must be a valid number`;
        },
    },
};
For nested objects, you can define nested handler objects. For arrays, a single handler applies to all array elements.

Basic Usage

The getFriendlyErrors function takes an array of validation errors and a map of error handler functions, returning an object with the same shape as your data type but with a single string error message for each failed property.

/* eslint-disable @typescript-eslint/no-unused-vars */
import {getFriendlyErrors} from '@mionkit/core';
import {RouteParamType, initClient} from '@mionkit/client';
import type {MyApi} from './friendly-errors-server.ts';
import {userFriendlyErrors} from './friendly-errors-map.ts';

const {routes} = initClient<MyApi>({baseURL: 'http://localhost:3000'});
type User = RouteParamType<MyApi['setUser'], 0>;

async function setUser() {
    const invalidUser = {
        name: 'A', // Too short (min 2)
        age: 5, // no errors
        balance: -100n, // Negative (min 0)
        isActive: 'yes' as unknown as boolean, // Wrong type
        tags: 'not-an-array' as unknown as string[], // Wrong type
        createdAt: 'invalid-date',
        nested: {
            email: 'invalid-email',
            score: -10, // Negative (min 0)
        },
    };
    const [user, error] = await routes.setUser(invalidUser as User).call();
    if (error?.type === 'validation-error') {
        const validationErrors = error.errorData?.typeErrors || [];
        // getFriendlyErrors() aggregates all errors per field and calls handler once with all params
        const friendlyErrors = getFriendlyErrors<User>(validationErrors, userFriendlyErrors);
        console.log(friendlyErrors.age); // undefined, age is valid
        console.log(friendlyErrors.name); // 'Name must be at least 2 characters' (single string per field)
        return;
    }
    if (error?.type === 'user-exists') {
        console.log('User already exists');
        return;
    }
    console.log('User created:', user);
}

Error Parameter Types

When using branded format types (like StrEmail, StrUrl, NumFormat, etc.), the error parameter types are automatically inferred. This means you get proper intellisense for the format parameters that might have failed without needing to manually specify the type.

// Define a simple type with format constraints
type ContactInfo = {
    name: StrFormat<{minLength: 2; maxLength: 50}>;
    email: StrEmail;
    website: StrFormat<{minLength: 10}>;
};

// Error params are automatically inferred from the branded type
// Handler is called ONCE per field with ALL aggregated error params
const contactErrors: FriendlyErrors<ContactInfo> = {
    // StrFormat infers StringErrorParams: minLength, maxLength, pattern, length, etc.
    // All failed constraints are available at once in the params object
    name: (failed) => {
        const messages: string[] = [];
        if (failed.minLength) messages.push(`at least ${failed.minLength.val} characters`);
        if (failed.maxLength) messages.push(`at most ${failed.maxLength.val} characters`);
        if (messages.length > 0) return `Name must be ${messages.join(' and ')}`;
        return 'Name must be a valid string';
    },
    // StrEmail infers EmailErrorParams: pattern, localPart, domain, minLength, maxLength
    email: (failed) => {
        const messages: string[] = [];
        if (failed.pattern) messages.push('invalid format');
        if (failed.localPart) messages.push('invalid username');
        if (failed.domain) messages.push('invalid domain');
        if (messages.length > 0) return `Email: ${messages.join(', ')}`;
        return 'Email must be a valid string';
    },
    // StrFormat infers StringErrorParams automatically
    website: (failed) => {
        if (failed.minLength) return `Website must be at least ${failed.minLength.val} characters`;
        return 'Website must be a valid string';
    },
};

Available Error Parameter Types

The following error parameter types are automatically inferred based on the branded type:

TypeUse For
StringErrorParamsStrFormat
EmailErrorParamsStrEmail
UrlErrorParamsStrUrl
DomainErrorParamsStrDomain
IPErrorParamsStrIP
UUIDErrorParamsStrUUID
DateTimeErrorParamsStrDateTime
DateErrorParamsStrDate
TimeErrorParamsStrTime
NumberErrorParamsNumFormat
BigIntErrorParamsBigNumFormat

All error parameter types also include:

  • rtError - The full RunTypeError object for access to expected, path, etc. (last error if multiple)
  • rtErrors - Array of all RunTypeError objects for this field (aggregated from all validation failures)
  • propName - The name of the property that failed validation
  • index - The array index (for array element errors)

Important Considerations

Error Aggregation Per Field

A single property can fail multiple validation constraints. For example, a string might fail both minLength and pattern validations. The getFriendlyErrors function aggregates all errors per field and calls the handler function once per field with all failed parameters available.

This means your handler receives all error params at once (e.g., both minLength AND maxLength if both failed), allowing you to generate a single comprehensive error message.

The result object contains a single string per field. When using the default error printer (no custom handler), multiple error messages are joined with '; '.

Accessing Error Values

Each error parameter (like minLength, max, etc.) is a TypeFormatError object containing:

  • val - The constraint value that was violated
  • name - The format name
  • formatPath - Path to the format parameter
// Access the constraint value from error parameters
const accessingErrorValues = (failed) => {
    if (failed.minLength) {
        const minValue = failed.minLength.val; // e.g., 5
        return `Must be at least ${minValue} characters`;
    }
    return 'Invalid value';
};

Default Error Printer

If no handler is provided for a property, getFriendlyErrors uses a default error printer and logs a warning to the console. The default messages are technical and not suitable for UI and end users.

Always provide custom handlers for user-facing applications.

Default messages look like: name: minLength validation failed (expected 5) - these are not user-friendly!

Root Level Errors

In rare cases, validation might fail at the root level (empty path). These errors are stored in a special $root property:

// Handle root level errors (when path is empty)
async function handleRootErrors() {
    const validationErrors: Parameters<typeof getFriendlyErrors>[0] = [];
    const result = getFriendlyErrors<User>(validationErrors, userFriendlyErrors);
    if (result.$root) {
        console.log('Root errors:', result.$root);
    }
}

Advanced Types

The FriendlyErrors<T> type supports advanced collection types including top-level arrays, Maps, and Sets.

Top-Level Arrays

For top-level array types (e.g., string[]), provide a single handler function that receives the index parameter:

/**
 * Example 1a: Top-level array of primitives with a single handler function
 * The handler is called for each array item that fails validation
 */
type TagList = string[];

export const tagListErrors: FriendlyErrors<TagList> = (failed) => {
    // failed.index contains the array index that failed
    const messages: string[] = [];
    if (failed.minLength) messages.push(`at least ${failed.minLength.val} characters`);
    if (failed.maxLength) messages.push(`at most ${failed.maxLength.val} characters`);
    if (messages.length > 0) {
        return `Tag at index ${failed.index} must be ${messages.join(' and ')}`;
    }
    return `Tag at index ${failed.index} must be a valid string`;
};

For arrays of complex objects, provide nested handlers that mirror the object structure:

/**
 * Example 1b: Top-level array of complex objects with nested handlers
 * The error map mirrors the structure of the array item type
 */
type UserList = User[];

export const userListErrors: FriendlyErrors<UserList> = {
    // Each property of User gets its own handler
    name: (failed) => {
        if (failed.minLength) return `Name must be at least ${failed.minLength.val} characters`;
        return 'Name is required';
    },
    email: (failed) => {
        return 'Email must be a valid email address';
    },
    age: (failed) => {
        if (failed.min) return `Age must be at least ${failed.min.val}`;
        if (failed.max) return `Age must be at most ${failed.max.val}`;
        return 'Age must be a valid number';
    },
};

Map Types

For Map<K, V> types, use $key and $value handlers to handle key and value validation errors separately:

/**
 * Example 2: Top-level Map with separate $key and $value handlers
 * - $key handler is called for key validation errors
 * - $value handler is called for value validation errors
 */
type UserMap = Map<string, User>;

export const userMapErrors: FriendlyErrors<UserMap> = {
    // Handler for key validation errors
    $key: (failed) => {
        // failed.index contains the position in the Map
        if (failed.minLength) {
            return `User ID at position ${failed.index} must be at least ${failed.minLength.val} characters`;
        }
        return `User ID at position ${failed.index} is invalid`;
    },
    // Handler for value validation errors (can be nested for complex values)
    $value: {
        name: (failed) => {
            if (failed.minLength) return `Name must be at least ${failed.minLength.val} characters`;
            return 'Name is required';
        },
        email: (failed) => {
            return 'Email must be a valid email address';
        },
        age: (failed) => {
            if (failed.min) return `Age must be at least ${failed.min.val}`;
            if (failed.max) return `Age must be at most ${failed.max.val}`;
            return 'Age must be a valid number';
        },
    },
};
The $value handler can be a nested object for complex value types, or a single function for primitive values.

Set Types

For Set<T> types, use the $item handler:

/**
 * Example 4a: Top-level Set of primitives with $item handler
 * The $item handler is called for each Set item that fails validation
 */
type RoleSet = Set<string>;

export const roleSetErrors: FriendlyErrors<RoleSet> = {
    $item: (failed) => {
        // failed.index contains the position in the Set
        if (failed.minLength) {
            return `Role at position ${failed.index} must be at least ${failed.minLength.val} characters`;
        }
        if (failed.maxLength) {
            return `Role at position ${failed.index} must be at most ${failed.maxLength.val} characters`;
        }
        return `Role at position ${failed.index} must be a valid string`;
    },
};

Reusable Error Maps

You can compose error maps by reusing existing maps in nested structures:

/**
 * Example 8: Reusable error map for User type
 * This can be used in multiple places where User validation is needed
 */
export const simpleUserErrors: FriendlyErrors<User> = {
    name: (failed) => {
        if (failed.minLength) return `Name must be at least ${failed.minLength.val} characters`;
        return 'Name is required';
    },
    email: (failed) => {
        return 'Email must be a valid email address';
    },
    age: (failed) => {
        if (failed.min) return `Age must be at least ${failed.min.val}`;
        if (failed.max) return `Age must be at most ${failed.max.val}`;
        return 'Age must be a valid number';
    },
};

// ============================================================================
// Complex Nested Example with Reusable Maps
// ============================================================================

/** Type with deeply nested collections */
interface Organization {
    name: string;
    /** Map of department name to list of users */
    departments: Map<string, User[]>;
    /** Set of admin user IDs */
    adminIds: Set<string>;
}

/**
 * Example 9: Complex nested structure with Map containing arrays of complex objects
 * Demonstrates reusing an existing error map (simpleUserErrors) inside another map
 */
export const organizationErrors: FriendlyErrors<Organization> = {
    name: (failed) => {
        if (failed.minLength) return `Organization name must be at least ${failed.minLength.val} characters`;
        return 'Organization name is required';
    },

    departments: {
        $key: (failed) => {
            return `Department name at position ${failed.index} is invalid`;
        },
        // $value is an array of Users (User[]), so we reuse the simpleUserErrors map
        // This demonstrates composing error maps - the nested User properties get their own handlers
        $value: simpleUserErrors,
    },

    adminIds: {
        $item: (failed) => {
            if (failed.minLength) {
                return `Admin ID at position ${failed.index} must be at least ${failed.minLength.val} characters`;
            }
            return `Admin ID at position ${failed.index} is invalid`;
        },
    },
};
mion's approach provides the most type-safe and flexible solution for transforming validation errors into user-friendly messages, with full support for complex nested types and format-specific error parameters.

Client-Side Validation

The mion client can validate parameters locally before sending requests:

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 : {...}}
}

This allows you to catch validation errors before making network requests, improving user experience and reducing server load.