kibinrpc

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 codeHTTP status
BAD_REQUEST400
UNAUTHORIZED401
FORBIDDEN403
NOT_FOUND404
METHOD_NOT_FOUND404
everything else500

Built-in error codes

CodeThrown byMeaning
NOT_FOUNDRouterNamespace does not exist
METHOD_NOT_FOUNDRouterMethod is not a registered action
BAD_REQUESTRouterInvalid JSON body, args is not an array, or batch size exceeds maxBatchSize
UNAUTHORIZEDYour codeThrow to signal authentication failure — maps to HTTP 401
FORBIDDENYour codeThrow to signal authorization failure — maps to HTTP 403
INTERNAL_ERRORRouterUnhandled exception (message hidden from client)
BATCH_MISMATCHClientServer returned fewer items than sent in a batch
TIMEOUTClientFetch attempt exceeded the configured timeout
ABORTEDClientRequest 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 code

On 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 passed

Typed 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.

On this page