Skip to content

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.

TypeScript
import { SurfError } from '@surfjs/core'
 
// Throw directly in any command handler
throw new SurfError('NOT_FOUND', 'Product not found', { id: params.sku })

Constructor

TypeScript
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:

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

TypeScript
import {
unknownCommand,
invalidParams,
authRequired,
authFailed,
sessionExpired,
rateLimited,
internalError,
notSupported,
notFound,
} from '@surfjs/core'

unknownCommand(command)

TypeScript
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?)

TypeScript
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?)

TypeScript
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?)

TypeScript
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?)

TypeScript
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?)

TypeScript
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?)

TypeScript
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)

TypeScript
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?)

TypeScript
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

TypeScript
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

TypeScript
const ipBlockMiddleware: SurfMiddleware = async (ctx, next) => {
if (blocklist.has(ctx.ip)) {
throw authFailed('IP address blocked')
}
await next()
}

Checking on the client

TypeScript
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():

TypeScript
import { assertNotPromise } from '@surfjs/core'
 
const surf = createSurf({ ... }) // ← missing await!
assertNotPromise(surf) // throws immediately with a clear message

This is called automatically by the HTTP and WebSocket handlers. It's most useful in tests or custom integrations where you build the handler manually.