A complete guide to schema-dsl error handling
I18nError - Multilingual error thrown
📖 Overview
I18nError is the unified multi-language error throwing mechanism provided by schema-dsl, specially designed for enterprise-level applications.
Core Value:
- ✅ Multi-language support: One set of codes, automatically adapted to Chinese/English/Japanese, etc.
- ✅ Unified Error Code: Use the same numerical code across languages, and front-end processing is not affected by language
- ✅ Parameter Interpolation: Supports dynamic parameters such as
{{#balance}}
- ✅ Framework Integration: Seamless integration with Express/Koa
- ✅ TypeScript support: complete type definitions
Applicable scenarios:
- API business logic errors (account does not exist, insufficient balance, insufficient permissions, etc.)
- Multilingual user scenarios (international applications)
- Systems that require unified error codes
Differences from ValidationError:
ValidationError: Form validation error (field-level error)
I18nError: Business logic error (application-level error)
🚀 Quick start
Get started in 5 minutes
import { I18nError, Locale } from 'schema-dsl/pure';
// Step 1: Configure language pack
Locale.addLocale('zh-CN', {
'account.notFound': {
code: 40001,
message: 'Account does not exist'
}
});
Locale.addLocale('en-US', {
'account.notFound': {
code: 40001,
message: 'Account not found'
}
});
// Step 2: Set default language
Locale.setLocale('zh-CN');
// Step 3: Use I18nError
try {
I18nError.throw('account.notFound');
} catch (error) {
console.log(error.message); // "Account does not exist"
console.log(error.code); // 40001
}
📚 Core API
I18nError.create()
Create error object (do not throw)
/**
* @param {string} code - error code (multi-language key)
* @param {Object|string} paramsOrLocale - parameter object or language code (intelligent recognition)
* @param {number} statusCode - HTTP status code (default 400)
* @param {string} locale - locale (optional)
* @returns {I18nError} error instance
*/
I18nError.create(code, paramsOrLocale?, statusCode?, locale?)
Usage Example:
//Basic usage
const error = I18nError.create('account.notFound');
//With parameters
const error = I18nError.create('account.insufficientBalance', {
balance: 50,
required: 100
});
//Specify status code
const error = I18nError.create('user.notFound', {}, 404);
// Specify language at runtime (v1.1.8+)
const error = I18nError.create('account.notFound', 'en-US', 404);
I18nError.throw()
Throw an error directly
/**
* @param {string} code - error code
* @param {Object|string} paramsOrLocale - parameter object or language code
* @param {number} statusCode - HTTP status code
* @param {string} locale - locale
* @throws {I18nError}
*/
I18nError.throw(code, paramsOrLocale?, statusCode?, locale?)
Usage Example:
// Throw an error directly
I18nError.throw('user.noPermission');
//With parameters and status code
I18nError.throw('account.insufficientBalance', { balance: 50, required: 100 }, 400);
// Simplified syntax (v1.1.8+)
I18nError.throw('account.notFound', 'zh-CN', 404);
I18nError.assert()
Assertion style - throw an error when the condition is not met
/**
* @param {any} condition - conditional expression (error thrown when falsy)
* @param {string} code - error code
* @param {Object|string} paramsOrLocale - parameter object or language code
* @param {number} statusCode - HTTP status code
* @param {string} locale - locale
* @throws {I18nError} thrown when the condition is false
*/
I18nError.assert(condition, code, paramsOrLocale?, statusCode?, locale?)
Usage Example:
function getAccount(id) {
const account = db.findAccount(id);
// Assert: The account must exist
I18nError.assert(account, 'account.notFound', { id });
// Assertion: The balance must be sufficient
I18nError.assert(
account.balance >= 100,
'account.insufficientBalance',
{ balance: account.balance, required: 100 }
);
return account;
}
s.error shortcut method
s.error is a shortcut to I18nError, providing the same three methods:
import { s } from 'schema-dsl/pure';
// Equivalent to I18nError.create()
s.error.create('account.notFound');
// Equivalent to I18nError.throw()
s.error.throw('order.notPaid');
// Equivalent to I18nError.assert()
s.error.assert(order, 'order.notFound');
Recommended usage scenarios:
- ✅ When used with
s() function (unified style)
- ✅ When importing fewer dependencies (only
dsl is required)
Method 1: Use Locale.addLocale() (recommended)
import { Locale } from 'schema-dsl/pure';
Locale.addLocale('zh-CN', {
//String format (simple scenario)
'user.notFound': 'User does not exist',
// Object format (recommended, v1.1.5+)
'account.notFound': {
code: 40001, // Numeric error code
message: 'Account does not exist'
},
'account.insufficientBalance': {
code: 40002,
message: 'Insufficient balance, current {{#balance}} yuan, need {{#required}} yuan'
}
});
Locale.addLocale('en-US', {
'user.notFound': 'User not found',
'account.notFound': {
code: 40001, //Same error code
message: 'Account not found'
},
'account.insufficientBalance': {
code: 40002,
message: 'Insufficient balance: {{#balance}}, required {{#required}}'
}
});
Method 2: Use s.config() (batch configuration)
import { s } from 'schema-dsl/pure';
s.config({
i18n: {
'zh-CN': {
'payment.failed': {
code: 50001,
message: 'Payment failed: {{#reason}}'
}
},
'en-US': {
'payment.failed': {
code: 50001,
message: 'Payment failed: {{#reason}}'
}
}
}
});
Way 3: Load from directory (large projects)
Directory structure:
project/
├── i18n/
│ └── errors/
│ ├── zh-CN.cjs
│ ├── en-US.jsonc
│ └── ja-JP.json5
└── app.js
Configuration:
import path from 'path';
s.config({
i18n: path.join(__dirname, 'i18n/errors')
});
Language pack file (for example i18n/errors/zh-CN.cjs):
module.exports = {
'account.notFound': {
code: 40001,
message: 'Account does not exist'
},
'account.insufficientBalance': {
code: 40002,
message: 'Insufficient balance, current {{#balance}} yuan, need {{#required}} yuan'
},
'user.noPermission': {
code: 40003,
message: 'You do not have permission to perform this operation'
}
};
🌐Default language mechanism
Default language settings
Default: 'en-US' (English)
Global settings:
import { Locale } from 'schema-dsl/pure';
//Switch the default language as needed by the application
Locale.setLocale('zh-CN');
// Get the current language
console.log(Locale.getLocale()); // 'zh-CN'
Language priority rules
Runtime locale parameters > global Locale.currentLocale > default 'en-US'
Example:
// Scenario 1: Using global language
Locale.setLocale('zh-CN');
I18nError.throw('account.notFound'); // Use Chinese 'zh-CN'
// Scenario 2: Runtime coverage
Locale.setLocale('zh-CN');
I18nError.throw('account.notFound', 'en-US'); // Override to English 'en-US'
// Scenario 3: Parameter object + runtime language
I18nError.throw('account.insufficientBalance',
{ balance: 50, required: 100 }, // parameter object
400,
'ja-JP' //Specify Japanese at runtime
);
Practical Application - API Multilingual Response
import express from 'express';
import { I18nError } from 'schema-dsl/pure';
const app = express();
//Middleware: extract client language
app.use((req, res, next) => {
req.locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
next();
});
//API routing
app.get('/api/account/:id', async (req, res) => {
try {
const account = await findAccount(req.params.id);
// 🎯 Specify language at runtime (according to client request)
I18nError.assert(account, 'account.notFound', req.locale, 404);
res.json({ success: true, data: account });
} catch (error) {
if (error instanceof I18nError) {
res.status(error.statusCode).json(error.toJSON());
} else {
res.status(500).json({ error: 'Internal Server Error' });
}
}
});
Effect:
- Client request header
Accept-Language: zh-CN → Return Chinese error
- Client request header
Accept-Language: en-US → Return English error
- No need to modify business code, automatic adaptation
Intelligent parameter recognition (v1.1.8)
v1.1.8 New: Support simplified syntax and intelligently identify the second parameter type
Simplified syntax
import { s, Locale } from 'schema-dsl/pure';
//Configure language pack
Locale.addLocale('zh-CN', {
'account.notFound': {
code: 40001,
message: 'Account does not exist'
}
});
Locale.addLocale('en-US', {
'account.notFound': {
code: 40001,
message: 'Account not found'
}
});
// ✅ New: Simplified syntax (recommended)
s.error.throw('account.notFound', 'zh-CN');
s.error.throw('account.notFound', 'zh-CN', 404);
// ✅ Standard syntax (fully compatible)
s.error.throw('account.notFound', {}, 404, 'zh-CN');
s.error.throw('account.notFound', { id: '123' }, 404, 'zh-CN');
Intelligent recognition rules
// Rules: Automatically determine the second parameter type
typeof params === 'string' → recognized as language parameters
typeof params === 'object' → recognized as parameter object
params === null/undefined → use default value
All calling methods
// 1. Simplified syntax - only pass the language
s.error.throw('account.notFound', 'zh-CN');
s.error.create('account.notFound', 'en-US');
s.error.assert(account, 'account.notFound', 'zh-CN');
// 2. Simplified syntax - language + status code
s.error.throw('account.notFound', 'zh-CN', 404);
s.error.assert(account, 'account.notFound', 'zh-CN', 404);
// 3. Standard syntax - object with parameters
s.error.throw('account.insufficientBalance',
{ balance: 50, required: 100 },
400,
'zh-CN'
);
// 4. Omit all parameters - use global language
s.error.throw('account.notFound');
Practical application
// Express API
app.get('/api/account/:id', async (req, res) => {
try {
const account = await findAccount(req.params.id);
const locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
// 🎯 Simplified syntax: only 2 parameters
s.error.assert(account, 'account.notFound', locale);
res.json(account);
} catch (error) {
res.status(error.statusCode).json(error.toJSON());
}
});
🌐 Actual scene
Express full integration
import express from 'express';
import { I18nError, Locale } from 'schema-dsl/pure';
const app = express();
app.use(express.json());
// ========== Configure language pack ==========
Locale.addLocale('zh-CN', {
'account.notFound': {
code: 40001,
message: 'Account does not exist'
},
'account.insufficientBalance': {
code: 40002,
message: 'Insufficient balance, current {{#balance}} yuan, need {{#required}} yuan'
}
});
Locale.addLocale('en-US', {
'account.notFound': {
code: 40001,
message: 'Account not found'
},
'account.insufficientBalance': {
code: 40002,
message: 'Insufficient balance: {{#balance}}, required {{#required}}'
}
});
// ========== Middleware: Extract language ==========
app.use((req, res, next) => {
req.locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
next();
});
// ========== Error handling middleware ==========
app.use((error, req, res, next) => {
if (error instanceof I18nError) {
return res.status(error.statusCode).json({
success: false,
error: error.toJSON()
});
}
// other errors
res.status(500).json({
success: false,
message: 'Internal Server Error'
});
});
// ========== Business routing ==========
app.get('/api/account/:id', async (req, res, next) => {
try {
const account = await findAccount(req.params.id);
// Use runtime language
I18nError.assert(account, 'account.notFound', req.locale, 404);
res.json({ success: true, data: account });
} catch (error) {
next(error);
}
});
app.post('/api/account/transfer', async (req, res, next) => {
try {
const { fromId, toId, amount } = req.body;
const account = await findAccount(fromId);
I18nError.assert(account, 'account.notFound', req.locale, 404);
I18nError.assert(
account.balance >= amount,
'account.insufficientBalance',
{ balance: account.balance, required: amount },
400,
req.locale
);
await transferMoney(fromId, toId, amount);
res.json({ success: true });
} catch (error) {
next(error);
}
});
Koa full integration
import Koa from 'koa';
import { I18nError, Locale } from 'schema-dsl/pure';
const app = new Koa();
// ========== Configure language pack ==========
Locale.addLocale('zh-CN', {
'user.noPermission': {
code: 40003,
message: 'You do not have permission to perform this operation'
}
});
// ========== Middleware: Extract language ==========
app.use(async (ctx, next) => {
ctx.locale = ctx.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
await next();
});
// ========== Error handling middleware ==========
app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
if (error instanceof I18nError) {
ctx.status = error.statusCode;
ctx.body = {
success: false,
error: error.toJSON()
};
} else {
ctx.status = 500;
ctx.body = { success: false, message: 'Internal Server Error' };
}
}
});
// ========== Business routing ==========
app.use(async (ctx) => {
if (ctx.path === '/api/admin/users' && ctx.method === 'GET') {
const user = await getCurrentUser(ctx);
I18nError.assert(user.role === 'admin', 'user.noPermission', ctx.locale, 403);
ctx.body = { success: true, data: await getUsers() };
}
});
Native Node.js HTTP Server
import http from 'http';
import { I18nError, Locale } from 'schema-dsl/pure';
//Configure language pack
Locale.addLocale('zh-CN', {
'order.notPaid': {
code: 50001,
message: 'Order not paid'
}
});
const server = http.createServer((req, res) => {
try {
//Extract language
const locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
// business logic
const order = getOrder(req.url);
I18nError.assert(order && order.paid, 'order.notPaid', locale, 400);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, data: order }));
} catch (error) {
if (error instanceof I18nError) {
res.writeHead(error.statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: false,
error: error.toJSON()
}));
} else {
res.writeHead(500);
res.end('Internal Server Error');
}
}
});
server.listen(3000);
TypeScript support
import { I18nError, Locale } from 'schema-dsl/pure';
// Type-safe language pack configuration
interface ErrorMessages {
[key: string]: {
code: number;
message: string;
};
}
const zhCN: ErrorMessages = {
'account.notFound': {
code: 40001,
message: 'Account does not exist'
}
};
Locale.addLocale('zh-CN', zhCN);
// Use type guards
function handleError(error: unknown): void {
if (error instanceof I18nError) {
console.log(`Error code: ${error.code}`);
console.log(`Error message: ${error.message}`);
console.log(`HTTP status: ${error.statusCode}`);
console.log(`Language: ${error.locale}`);
}
}
//Business function
async function getAccount(id: string): Promise<Account> {
const account = await findAccount(id);
I18nError.assert(account, 'account.notFound', { id }, 404);
return account;
}
📦 Error object structure
try {
I18nError.throw('account.notFound', {}, 404);
} catch (error) {
console.log(error.toJSON());
}
Output:
{
"error": "I18nError",
"originalKey": "account.notFound",
"code": 40001,
"message": "Account does not exist",
"params": {},
"statusCode": 404,
"locale": "zh-CN"
}
Field description:
error: fixed to "I18nError"
originalKey: Original error key (new in v1.1.5, used for log tracking)
code: Error code (number or string)
message: Translated error message
params: parameter object
statusCode: HTTP status code
locale: Language used
Error object properties
try {
I18nError.throw('account.insufficientBalance',
{ balance: 50, required: 100 },
400,
'zh-CN'
);
} catch (error) {
console.log(error.name); // 'I18nError'
console.log(error.message); // 'The balance is insufficient, currently 50 yuan, 100 yuan is needed'
console.log(error.originalKey); // 'account.insufficientBalance'
console.log(error.code); // 40002
console.log(error.params); // { balance: 50, required: 100 }
console.log(error.statusCode); // 400
console.log(error.locale); // 'zh-CN'
console.log(error.stack); // stack trace
}
is() method - wrong type determination
try {
I18nError.throw('account.notFound');
} catch (error) {
if (error instanceof I18nError) {
// Use originalKey to judge
if (error.is('account.notFound')) {
console.log('Account does not exist error');
}
// Use digital code to judge (v1.1.5+)
if (error.is(40001)) {
console.log('There is no error in the account (judged by digital code)');
}
}
}
❓ FAQ
Q1: How to dynamically switch languages?
A: There are two ways:
// Method 1: Global switching (affects all subsequent calls)
Locale.setLocale('en-US');
I18nError.throw('account.notFound'); // Use English
// Method 2: Specified at runtime (only affects the current call)
I18nError.throw('account.notFound', 'en-US'); // Use English
I18nError.throw('account.notFound', 'zh-CN'); // Use Chinese
Recommended: Dynamically specified in the API based on client request headers (see Express example above)
A:
//String format
'user.notFound': 'User does not exist'
// Object format (recommended)
'user.notFound': {
code: 40001, // Unified digital code
message: 'User does not exist'
}
Recommendation: Prioritize the use of object format to facilitate unified processing by the front end.
Q3: How to use parameter interpolation?
A: Use {{#parameterName}} syntax:
// Language pack configuration
Locale.addLocale('zh-CN', {
'account.insufficientBalance': {
code: 40002,
message: 'Insufficient balance, current {{#balance}} yuan, need {{#required}} yuan'
}
});
// use
I18nError.throw('account.insufficientBalance', {
balance: 50,
required: 100
});
// Output: "Insufficient balance, current 50 yuan, 100 yuan needed"
Note: The parameter name must be in the format {{#parameterName}} (the pound sign must be present).
Q4: What is the difference with message() of s.if?
A:
s.if().message(): Used for data validation errors (Schema validation)
I18nError: used for business logic error (API business logic)
// s.if - data validation
s.if(d => !d).message('user.notFound').assert(user);
// I18nError - business logic
I18nError.assert(user.role === 'admin', 'user.noPermission');
Can be mixed:
function validateAndProcess(user) {
// Step 1: Data validation (using s.if)
s.if(d => !d).message('user.notFound').assert(user);
// Step 2: Business logic validation (using I18nError)
I18nError.assert(user.role === 'admin', 'user.noPermission');
}
Q5: How to get all available languages?
A:
import { Locale } from 'schema-dsl/pure';
const locales = Locale.getAvailableLocales();
console.log(locales); // ['en-US', 'zh-CN', 'ja-JP', ...]
A: Use numeric code field:
// Front-end error handling
async function apiCall() {
try {
const response = await fetch('/api/account');
const data = await response.json();
} catch (error) {
// Unified processing based on digital code (not affected by language)
switch (error.code) {
case 40001:
router.push('/login'); // Account does not exist → Jump to login
break;
case 40002:
showTopUpDialog(); // Insufficient balance → Show recharge pop-up window
break;
case 40003:
showError('Insufficient permissions'); // Insufficient permissions
break;
default:
showError(error.message);
}
}
}
Advantages: Front-end logic is not affected by back-end language switching.
Q7: What is the default language? How to modify?
A:
- Default language:
'en-US' (English)
- Modification method:
import { Locale } from 'schema-dsl/pure';
//Set the default language according to the application needs at startup
Locale.setLocale('zh-CN');
// Get the current default language
console.log(Locale.getLocale()); // 'zh-CN'
Recommendation: Set the default language when the application starts (app.js entry).
A: If the error key is not configured in the language package, the original key will be returned directly:
// 'custom.error' is not configured
I18nError.throw('custom.error');
// message: 'custom.error' (returned as is)
suggestion:
- Use TypeScript to define incorrect key types and avoid typos
- Check in the development environment whether all error keys have been configured
Q9: What built-in languages are supported?
A:
Custom Language: Use Locale.addLocale() to add any language.
Q10: How to record error details in the log?
A:
import winston from 'winston';
app.use((error, req, res, next) => {
if (error instanceof I18nError) {
// Record detailed logs
winston.error('Business error', {
originalKey: error.originalKey, // Original key (for easy tracking)
code: error.code, // error code
message: error.message, // Translated message
params: error.params, // parameters
statusCode: error.statusCode,
locale: error.locale,
url: req.url,
method: req.method,
ip: req.ip
});
return res.status(error.statusCode).json(error.toJSON());
}
next(error);
});
RECOMMENDED: Use originalKey instead of message because message changes with the language.
error object structure
infrastructure
The error object structure returned by schema-dsl validation:
import { s, validate } from 'schema-dsl/pure';
const schema = s({
username: s('string:3-32!').label('username')
});
const result = validate(schema, { username: 'ab' });
//return structure
{
valid: false,
errors: [
{
path: 'username',
field: 'username',
keyword: 'minLength',
params: { limit: 3 },
message: 'Username cannot be less than 3 characters long'
}
]
}
Nested object error
import { s, validate } from 'schema-dsl/pure';
const schema = s({
user: {
profile: {
email: 'email!'
}
}
});
const result = validate(schema, {
user: {
profile: {
email: 'invalid'
}
}
});
//wrong path
console.log(result.errors[0].path); // 'user/profile/email'
console.log(result.errors[0].message); // 'The email must be a valid email address'
array item error
import { s, validate } from 'schema-dsl/pure';
const schema = s({
items: 'array<string:3->!'
});
const result = validate(schema, {
items: ['ab', 'valid']
});
//wrong path
console.log(result.errors[0].path); // 'items/0'
Error message customization
Single field customization
import { s } from 'schema-dsl/pure';
//Use String to extend custom messages
const schema = s({
username: s('string:3-32!').label('username')
.messages({
'min': 'Too short! At least 3 characters'
})
});
Multi-rule customization
import { s } from 'schema-dsl/pure';
const schema = s({
email: s('email!').label('email address')
.messages({
'format': 'The email format is wrong',
'required': 'The mailbox cannot be empty'
})
});
Object level customization
import { s } from 'schema-dsl/pure';
const schema = s({
username: s('string:3-32!').label('username')
.messages({
'min': '{{#label}}at least {{#limit}} characters',
'max': '{{#label}} is at most {{#limit}} characters'
}),
email: s('email!').label('mailbox')
.messages({
'format': '{{#label}} format is invalid'
})
});
Global customization
import { Locale } from 'schema-dsl/pure';
//Set global message
Locale.setMessages({
'min': 'The input is too short, {{#limit}} characters are required',
'format': 'Incorrect format'
});
error code system
Built-in error codes (simplified version)
schema-dsl uniformly formats Ajv's error keywords to make it easier to use:
String error code
Numeric error code
Common error code
💡 Tip: You can customize the error message using simplified keywords (such as min) or original keywords (such as minLength), and the system will automatically handle the mapping.
Automatic label translation
If you define label.{fieldName} in a language pack, the system will automatically use it as a Label without explicitly calling .label().
// language pack
Locale.addLocale('zh-CN', {
'label.username': 'username',
'required': '{{#label}} cannot be empty'
});
// Schema
const schema = s({
username: 'string!' // Automatically find label.username
});
// Error message: "Username cannot be empty"
Custom validation errors
import { s } from 'schema-dsl/pure';
const schema = s({
username: s('string:3-32!').custom((value) => {
if (value.includes('forbidden')) {
return 'The content contains prohibited words';
}
// No need to return when validation passes
})
.label('username')
});
Multi-level error handling
Nested object validation
import { s, validate } from 'schema-dsl/pure';
const schema = s({
user: {
name: 'string:1-100!',
address: {
country: s('string!').label('country'),
city: s('string!').label('city'),
street: s('string!').label('street')
}
}
});
const result = validate(schema, {
user: {
name: 'John',
address: {
country: 'CN'
// missing city and street
}
}
});
// Error example
// result.errors[0].path: 'user/address/city'
// result.errors[1].path: 'user/address/street'
Array validation
import { s, validate } from 'schema-dsl/pure';
const schema = s({
items: s('array:1-<string:3->!').label('Product List')
});
const result = validate(schema, {
items: ['ab', 'valid'] // The first item is too short
});
//wrong path
console.log(result.errors[0].path); // 'items/0'
API responsive design
// successful response
{
success: true,
code: 'SUCCESS',
data: { ... }
}
//Verify error response
{
success: false,
code: 'VALIDATION_ERROR',
message: 'Data validation failed',
errors: [
{
field: 'username',
message: 'must NOT have fewer than 3 characters',
keyword: 'minLength',
params: { limit: 3 }
}
]
}
// Server error response
{
success: false,
code: 'SERVER_ERROR',
message: 'Server internal error'
}
Express middleware
import { s, Validator } from 'schema-dsl/pure';
//Authentication middleware
function validateBody(schema) {
const validator = new Validator();
return (req, res, next) => {
const result = validator.validate(schema, req.body);
if (!result.valid) {
return res.status(400).json({
success: false,
code: 'VALIDATION_ERROR',
message: 'Please check the input information',
errors: result.errors.map(err => ({
field: err.path.replace(/\//g, '.'),
message: err.message,
keyword: err.keyword,
params: err.params
}))
});
}
// Validation passed, continue processing
next();
};
}
// Usage example
const userSchema = s({
username: 'string:3-32!',
email: 'email!',
password: 'string:8-64!'
});
app.post('/api/users',
validateBody(userSchema),
async (req, res) => {
const user = await createUser(req.body);
res.json({ success: true, data: user });
}
);
Koa middleware
import { s, Validator } from 'schema-dsl/pure';
function validateBody(schema) {
const validator = new Validator();
return async (ctx, next) => {
const result = validator.validate(schema, ctx.request.body);
if (!result.valid) {
ctx.status = 400;
ctx.body = {
success: false,
code: 'VALIDATION_ERROR',
message: 'Data validation failed',
errors: result.errors.map(err => ({
field: err.path.replace(/\//g, '.'),
message: err.message,
keyword: err.keyword
}))
};
return;
}
await next();
};
}
// Usage example
const registerSchema = s({
username: s('string:3-32!').username(),
email: 'email!',
password: s('string!').password('strong')
});
router.post('/register', validateBody(registerSchema), async (ctx) => {
ctx.body = { success: true, data: await register(ctx.request.body) };
});
Front-end error display
React example
import React, { useState } from 'react';
function RegisterForm() {
const [errors, setErrors] = useState({});
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const data = await response.json();
if (!data.success && data.code === 'VALIDATION_ERROR') {
//Convert error array to object
const errorMap = {};
data.errors.forEach(err => {
errorMap[err.field] = err.message;
});
setErrors(errorMap);
}
} catch (error) {
console.error(error);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input name="username" />
{errors.username && (
<span className="error">{errors.username}</span>
)}
</div>
<div>
<input name="email" type="email" />
{errors.email && (
<span className="error">{errors.email}</span>
)}
</div>
<button type="submit">Register</button>
</form>
);
}
Vue example
<template>
<form @submit.prevent="handleSubmit">
<div>
<input v-model="form.username" />
<span v-if="errors.username" class="error">
{{ errors.username }}
</span>
</div>
<div>
<input v-model="form.email" type="email" />
<span v-if="errors.email" class="error">
{{ errors.email }}
</span>
</div>
<button type="submit">Register</button>
</form>
</template>
<script>
export default {
data() {
return {
form: {
username: '',
email: ''
},
errors: {}
};
},
methods: {
async handleSubmit() {
try {
const response = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.form)
});
const data = await response.json();
if (!data.success && data.code === 'VALIDATION_ERROR') {
this.errors = data.errors.reduce((acc, err) => {
acc[err.field] = err.message;
return acc;
}, {});
}
} catch (error) {
console.error(error);
}
}
}
};
</script>
Error logging
Basic log
app.post('/api/register', async (req, res) => {
const result = await registerSchema.validate(req.body, {
abortEarly: false
});
if (!result.isValid) {
// Record validation errors
logger.warn('User registration validation failed', {
ip: req.ip,
errors: result.errors,
data: req.body
});
return res.status(400).json({
success: false,
errors: result.errors
});
}
// continue processing
});
Structured log
import logger from 'winston';
function logValidationError(req, result) {
logger.warn({
message: 'Authentication failed',
type: 'VALIDATION_ERROR',
timestamp: new Date().toISOString(),
ip: req.ip,
url: req.url,
method: req.method,
errors: result.errors.map(err => ({
path: err.path.replace(/\//g, '.'),
type: err.type,
message: err.message
})),
// Sensitive data desensitization
data: maskSensitiveData(req.body)
});
}
best practices
1. Use labels to make error messages clearer
import { s } from 'schema-dsl/pure';
// ✅ Recommendation: use label
const schema = s({
username: s('string:3-32!').label('username')
});
// The error message will contain the "username" tag
// ❌ Not recommended: do not use label
const schema = s({
username: 'string:3-32!'
});
// The error message only displays the field name "username"
2. Provide friendly Chinese error messages
import { s } from 'schema-dsl/pure';
// ✅ Recommended: Customized Chinese message
const schema = s({
username: s('string:3-32!').label('username')
.messages({
'minLength': '{{#label}} requires at least {{#limit}} characters',
'maxLength': '{{#label}} is at most {{#limit}} characters'
})
});
// ❌ Not recommended: use the default English message
const schema = s({
username: 'string:3-32!'
});
3. Use custom validation to implement business logic
import { s } from 'schema-dsl/pure';
// ✅ Recommended: Return error message string
const schema = s({
username: s('string:3-32!').custom((value) => {
if (value === 'admin') {
return 'Username has been occupied';
}
// No need to return when validation passes
})
.label('username')
});
4. Do not include sensitive data in error logs
function maskSensitiveData(data) {
return {
...data,
password: '***',
confirmPassword: '***',
creditCard: data.creditCard ? '****' + data.creditCard.slice(-4) : undefined
};
}
// use
logger.warn('Validation failed', {
errors: result.errors,
data: maskSensitiveData(req.body)
});
// Unified error formatting function
function formatValidationErrors(errors) {
return errors.map(err => ({
field: err.path.replace(/\//g, '.'),
message: err.message,
keyword: err.keyword,
params: err.params
}));
}
// use
if (!result.valid) {
return res.status(400).json({
success: false,
code: 'VALIDATION_ERROR',
errors: formatValidationErrors(result.errors)
});
}
Overview
Starting from v1.1.5, the language pack supports the object format { code, message } to achieve unified error code management.
Basic usage
Language pack configuration:
// i18n/errors/zh-CN.cjs (or any.json/.jsonc/.json5 custom language pack file)
module.exports = {
// String format (backwards compatible)
'user.notFound': 'User does not exist',
// Object format (new in v1.1.5) ✨ - Use numeric error codes
'account.notFound': {
code: 40001,
message: 'Account does not exist'
},
'account.insufficientBalance': {
code: 40002,
message: 'Insufficient balance, current balance {{#balance}}, need {{#required}}'
},
'order.notPaid': {
code: 50001,
message: 'Order not paid'
}
};
Usage Example:
import { s } from 'schema-dsl/pure';
try {
s.error.throw('account.notFound');
} catch (error) {
console.log(error.originalKey); // 'account.notFound'
console.log(error.code); // 40001 ✨ Numeric error code
console.log(error.message); // 'Account does not exist'
}
Core features
1. originalKey field (new)
Keep the original key for easy debugging and log tracking:
try {
s.error.throw('account.notFound');
} catch (error) {
error.originalKey // 'account.notFound' (original key)
error.code // 40001 (numeric error code)
}
2. Sharing code in multiple languages
Different languages use the same number code to facilitate unified processing by the front end:
// zh-CN.cjs
'account.notFound': {
code: 40001, // ← digital code consistent
message: 'Account does not exist'
}
// en-US.cjs
'account.notFound': {
code: 40001, // ← digital code consistent
message: 'Account not found'
}
// Front-end processing - language independent
switch (error.code) {
case 40001:
redirectToLogin();
break;
case 40002:
showTopUpDialog();
break;
case 50001:
showPaymentDialog();
break;
}
3. Enhanced error.is() method
Supports originalKey and digital code judgments at the same time:
try {
s.error.throw('account.notFound');
} catch (error) {
// Both methods are available
if (error.is('account.notFound')) { } // ✅ Use originalKey
if (error.is(40001)) { } // ✅ Use numeric code
}
4. toJSON contains originalKey
const json = error.toJSON();
// {
// error: 'I18nError',
// originalKey: 'account.notFound', // ✨ v1.1.5 New
// code: 'ACCOUNT_NOT_FOUND',
// message: 'Account does not exist',
// params: {},
// statusCode: 400,
// locale: 'zh-CN'
// }
backwards compatible
Fully Backward Compatible ✅ - Automatic string format conversion:
// String format (original)
'user.notFound': 'User does not exist'
// Automatically convert to object
s.error.throw('user.notFound');
// error.code = 'user.notFound' (use key as code)
// error.originalKey = 'user.notFound'
// error.message = 'User does not exist'
best practices
Recommended to use object format:
- ✅ Errors that need to be handled uniformly in multiple languages
- ✅ Errors that require unified judgment by the front end
- ✅ Core business errors (account, order, payment, etc.)
String format can be used:
- ✅ Simple validation error
- ✅ Internal errors (not exposed to front-end)
- ✅ Errors that do not need to be handled uniformly
2. Error code naming convention
It is recommended to use numeric error codes, segmented by module:
// Error code specification (5 digits)
// 4xxxx - client error
// 5xxxx - Business logic error
// 6xxxx - System error
'account.notFound': {
code: 40001, // ✅ Recommended: Account module, serial number 001
message: 'Account does not exist'
}
'account.insufficientBalance': {
code: 40002, // Account module, serial number 002
message: 'Insufficient balance'
}
'order.notPaid': {
code: 50001, // ✅ Order module, serial number 001
message: 'Order not paid'
}
'order.cancelled': {
code: 50002, // Order module, serial number 002
message: 'Order has been cancelled'
}
'database.connectionError': {
code: 60001, // ✅ System error
message: 'Database connection failed'
}
Error code segmentation suggestions:
40001-49999 - Client error (account, permission, parameter validation, etc.)
50001-59999 - Business logic error (order, payment, inventory, etc.)
60001-69999 - System error (database, service unavailable, etc.)
3. Unified error handling on the front end
//API call
try {
const response = await fetch('/api/account');
const data = await response.json();
} catch (error) {
//Use digital code for unified processing, not affected by language
switch (error.code) {
case 40001: // ACCOUNT_NOT_FOUND
showNotFoundPage();
break;
case 40002: // INSUFFICIENT_BALANCE
showTopUpDialog(error.params);
break;
case 50001: // ORDER_NOT_PAID
showPaymentDialog();
break;
case 60001: // SYSTEM_ERROR
showSystemErrorPage();
break;
default:
showGenericError(error.message);
}
}
More elegant way - error code mapping:
// errorCodeMap.js
const ERROR_HANDLERS = {
40001: () => router.push('/account-not-found'),
40002: (error) => showDialog('topup', error.params),
50001: (error) => showDialog('payment', error.params),
60001: () => showSystemErrorPage(),
};
// Unified error handling
function handleError(error) {
const handler = ERROR_HANDLERS[error.code];
if (handler) {
handler(error);
} else {
showGenericError(error.message);
}
}
Corresponding sample file
Example entry: error-handling.ts
Description: Covers the field errors generated by validate(), I18nError business error objects, toJSON() output and error code judgment.