Core API
SurfError
Typed error class and convenience constructors for server-side error handling
SurfError#
SurfError is the typed error class used for all Surf server errors. Every error that reaches the client carries a machine-readable code and optional details — no raw Error objects in the public API.
import { SurfError } from '@surfjs/core' // Throw directly in any command handlerthrow new SurfError('NOT_FOUND', 'Product not found', { id: params.sku })Constructor
new SurfError( code: SurfErrorCode, // machine-readable error code message: string, // human-readable message details?: Record<string, unknown> // optional structured context)Properties
| Property | Type | Description |
|----------|------|-------------|
| code | SurfErrorCode | Machine-readable error code from the Surf spec |
| message | string | Human-readable description |
| details | Record<string, unknown>? | Optional structured context (field names, IDs, etc.) |
| name | string | Always 'SurfError' — useful for instanceof checks |
.toJSON()
Serializes to a plain object suitable for the wire format:
const err = new SurfError('NOT_FOUND', 'Order not found', { orderId: '123' })err.toJSON()// => { code: 'NOT_FOUND', message: 'Order not found', details: { orderId: '123' } }Convenience Constructors#
Use these factory functions instead of constructing SurfError manually. They encode the correct code and produce consistent messages.
import { unknownCommand, invalidParams, authRequired, authFailed, sessionExpired, rateLimited, internalError, notSupported, notFound,} from '@surfjs/core'unknownCommand(command)
throw unknownCommand('canvas.addCircle')// SurfError { code: 'UNKNOWN_COMMAND', message: 'Unknown command: canvas.addCircle' }Surf throws this automatically for unregistered commands. Use it in custom proxy or middleware layers.
invalidParams(message, details?)
throw invalidParams('quantity must be positive', { field: 'quantity', value: -1 })// SurfError { code: 'INVALID_PARAMS', message: 'quantity must be positive', details: { field, value } }Thrown automatically by Zod validators and Surf's built-in param validation. Throw manually for business-logic validation that can't be expressed in a schema.
authRequired(command?)
throw authRequired() // 'Authentication required'throw authRequired('admin.analytics') // 'Authentication required for command: admin.analytics'Thrown automatically by Surf when a request reaches an auth: 'required' command without a token.
authFailed(reason?)
throw authFailed() // 'Authentication failed'throw authFailed('Token expired') // 'Token expired'Thrown when the auth token is present but invalid (returned valid: false from authVerifier).
sessionExpired(sessionId?)
throw sessionExpired()// SurfError { code: 'SESSION_EXPIRED', message: 'Session expired or not found' }Use in session-aware commands when the session is no longer valid. Clients receive HTTP 410.
rateLimited(retryAfterMs?)
throw rateLimited(30_000)// SurfError { code: 'RATE_LIMITED', details: { retryAfterMs: 30000 } }Thrown automatically by Surf's built-in RateLimiter. Clients receive HTTP 429 with a Retry-After header derived from retryAfterMs.
internalError(message?)
throw internalError() // 'Internal server error'throw internalError('Failed to reach DB') // 'Failed to reach DB'Use when an unexpected failure occurs that shouldn't expose internal details. In production, Surf strips the message from the response; in debug mode the message is included.
notSupported(command)
throw notSupported('canvas.export')// SurfError { code: 'NOT_SUPPORTED', message: 'Command not available: canvas.export' }Use when a command exists in the manifest but isn't available in the current environment (e.g. a browser-only command hit via HTTP).
notFound(resource, id?)
throw notFound('Product', params.sku) // 'Product not found: LAPTOP-01'throw notFound('Workspace') // 'Workspace not found'Use for domain-level "not found" errors. Clients receive HTTP 404.
Error Codes#
All codes are defined in the SurfErrorCode union type from @surfjs/core.
| Code | HTTP | Thrown by | Description |
|------|------|-----------|-------------|
| UNKNOWN_COMMAND | 404 | Surf (auto) | Command not registered |
| INVALID_PARAMS | 400 | Surf (auto) + manually | Missing required field or wrong type |
| AUTH_REQUIRED | 401 | Surf (auto) + manually | auth: 'required' but no token sent |
| AUTH_FAILED | 403 | Surf (auto) + manually | Token present but invalid |
| SESSION_EXPIRED | 410 | Manually | Session no longer valid |
| RATE_LIMITED | 429 | RateLimiter (auto) | Too many requests |
| INTERNAL_ERROR | 500 | Surf (auto) + manually | Unexpected server failure |
| NOT_SUPPORTED | 501 | Surf (auto) + manually | Feature not available |
| NOT_FOUND | 404 | Manually | Domain resource not found |
Usage Patterns#
In a command handler
const surf = await createSurf({ commands: { 'order.cancel': { description: 'Cancel an order', auth: 'required', params: { orderId: { type: 'string', required: true } }, run: async ({ orderId }, ctx) => { const order = await db.orders.get(orderId) if (!order) throw notFound('Order', orderId) if (order.userId !== ctx.claims?.userId) throw authFailed('Not your order') if (order.status === 'shipped') { throw new SurfError('NOT_SUPPORTED', 'Cannot cancel a shipped order', { status: order.status, }) } return db.orders.cancel(orderId) }, }, },})In middleware
const ipBlockMiddleware: SurfMiddleware = async (ctx, next) => { if (blocklist.has(ctx.ip)) { throw authFailed('IP address blocked') } await next()}Checking on the client
import { SurfClientError } from '@surfjs/client' try { await client.execute('order.cancel', { orderId })} catch (err) { if (err instanceof SurfClientError) { switch (err.code) { case 'NOT_FOUND': return showNotFound() case 'AUTH_REQUIRED': return redirectToLogin() case 'RATE_LIMITED': return retryAfter(err.retryAfter) default: return showGenericError(err.message) } }}assertNotPromise
Guards against a common mistake — forgetting to await createSurf():
import { assertNotPromise } from '@surfjs/core' const surf = createSurf({ ... }) // ← missing await!assertNotPromise(surf) // throws immediately with a clear messageThis is called automatically by the HTTP and WebSocket handlers. It's most useful in tests or custom integrations where you build the handler manually.