Error Handling
Structured errors from server to client with KibinError.
On the server
Throw KibinError to send a structured error to the client:
import { KibinError } from '@kibinrpc/server'
throw new KibinError('NOT_FOUND', 'User not found')
throw new KibinError('UNAUTHORIZED', 'Invalid token')
throw new KibinError('BAD_REQUEST', 'Invalid input')Any other thrown value (a plain Error, a string, etc.) becomes { code: 'INTERNAL_ERROR' }. The original message is not sent to the client to avoid leaking internal details.
HTTP status mapping
| Error code | HTTP status |
|---|---|
BAD_REQUEST | 400 |
UNAUTHORIZED | 401 |
FORBIDDEN | 403 |
NOT_FOUND | 404 |
METHOD_NOT_FOUND | 404 |
| everything else | 500 |
Built-in error codes
| Code | Thrown by | Meaning |
|---|---|---|
NOT_FOUND | Router | Namespace does not exist |
METHOD_NOT_FOUND | Router | Method is not a registered action |
BAD_REQUEST | Router | Invalid JSON body, args is not an array, or batch size exceeds maxBatchSize |
UNAUTHORIZED | Your code | Throw to signal authentication failure — maps to HTTP 401 |
FORBIDDEN | Your code | Throw to signal authorization failure — maps to HTTP 403 |
INTERNAL_ERROR | Router | Unhandled exception (message hidden from client) |
BATCH_MISMATCH | Client | Server returned fewer items than sent in a batch |
TIMEOUT | Client | Fetch attempt exceeded the configured timeout |
ABORTED | Client | Request was cancelled via signal |
isKibinError
isKibinError is available from both packages — use whichever you're already importing:
import { isKibinError } from '@kibinrpc/server' // in server interceptors
import { isKibinError } from '@kibinrpc/client' // in client codeOn the client
Use isKibinError to distinguish structured server errors from network failures or unexpected exceptions:
import { isKibinError } from '@kibinrpc/client'
try {
const user = await client.user.getUser('999')
} catch (err) {
if (isKibinError(err)) {
// Structured error from the server
console.log(err.code) // e.g. 'NOT_FOUND'
console.log(err.message) // e.g. 'User not found'
} else {
// Network error or unexpected exception
console.error(err)
}
}isKibinError is a TypeScript type guard — inside the if block, err is typed as KibinError.
KibinError type
import type { KibinError } from '@kibinrpc/client'
err.code // string — the error code from the server
err.message // string — the human-readable message
err.cause // unknown — the original error, if one was passedTyped error codes
KibinError accepts a generic type parameter so the code property can be narrowed to a string literal:
import { KibinError } from '@kibinrpc/server'
// TypeScript infers KibinError<'NOT_FOUND'>
throw new KibinError('NOT_FOUND', 'User not found')
// Explicit type — useful for function signatures
function forbidden(): never {
throw new KibinError<'UNAUTHORIZED'>('UNAUTHORIZED', 'Invalid token')
}isKibinError accepts an optional second argument to check the code at runtime and narrow the type in one step:
import { isKibinError } from '@kibinrpc/client'
try {
await client.user.getUser('999')
} catch (err) {
if (isKibinError(err, 'NOT_FOUND')) {
// err.code is typed as 'NOT_FOUND'
console.log(err.message)
}
}KibinError without a type argument defaults to KibinError<string>, so all existing code remains valid.
Error chaining with cause
Pass a cause to preserve the original error when wrapping exceptions on the server:
import { KibinError } from '@kibinrpc/server'
async function getUser(id: string) {
try {
return await db.users.findById(id)
} catch (err) {
throw new KibinError('INTERNAL_ERROR', 'Database error', { cause: err })
}
}The cause stays on the server — it follows the standard ES2022 Error.cause convention and is never sent to the client.
Batched calls
In a batched request, each item has its own error or data. Failed items throw individually — a partial failure does not reject the whole Promise.all:
try {
const [users, post] = await Promise.all([
client.user.listUsers(), // succeeds
client.post.getPost('missing'), // throws NOT_FOUND
])
} catch (err) {
if (isKibinError(err)) {
console.log(err.code) // 'NOT_FOUND'
}
}The HTTP response for that batch is 207 Multi-Status. The client unpacks each item and resolves or rejects each promise independently.
Error interceptor
Handle errors centrally with the client error interceptor:
const client = createKibinClient<AppRouter>({
baseUrl: '/api/rpc',
interceptors: {
error({ error }) {
if (error.code === 'UNAUTHORIZED') {
window.location.href = '/login'
return // suppress — redirect handles it
}
throw error // rethrow everything else
},
},
})Returning from the error interceptor (instead of throwing) resolves the promise with undefined. Rethrowing propagates the error to the caller.