Skip to content

Error Handling

SpacetimeDB: Error Handling ↗

Declared errors are tagged errors with a namespace prefix. The namespace keeps wire tags globally unique within a module, while each field uses a normal Stdb.* value type.

import * as Effect from "effect/Effect"
import * as Stdb from "effect-spacetimedb"
const AppErrors = Stdb.errors.namespace("App")({
UserMissing: Stdb.error({ userId: Stdb.string() }),
})
const Users = Stdb.StdbGroup.make("Users").add(
Stdb.StdbFn.reducer("user_require", {
params: Stdb.struct({ userId: Stdb.string() }),
errors: AppErrors,
}),
Stdb.StdbFn.procedure("user_get", {
params: Stdb.struct({ userId: Stdb.string() }),
returns: Stdb.option(Stdb.struct({ id: Stdb.string() })),
errors: AppErrors,
}),
)
const UserRoutes = Stdb.StdbHttpGroup.make("UserRoutes").add(
Stdb.StdbHttp.post("user_token_rotate", "/users/token/rotate", {
request: Stdb.struct({ userId: Stdb.string() }).schema,
response: Stdb.struct({ token: Stdb.string() }).schema,
errors: AppErrors,
}),
)
const Module = Stdb.StdbModule.make("app").add(Users).add(UserRoutes)
const fail = (userId: string) =>
Effect.gen(function* () {
return yield* new AppErrors.UserMissing({ userId })
})

Attach the error catalog with errors: and fail with the generated classes directly. Declared errors are encoded on the wire with a versioned envelope, so plain host rejection strings are not confused with domain failures.

An error can optionally declare an HTTP status with Stdb.error(fields, { status }). That status applies only when the error surfaces over an HTTP route; on the reducer and procedure (WebSocket) path there is no HTTP response, so it is ignored.

Clients receive declared tagged errors directly from reducer and procedure calls, and typed HTTP routes deliver the same errors through the projected Effect HttpApi client:

import * as Effect from "effect/Effect"
import * as HttpClient from "effect/unstable/http/HttpClient"
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient"
import * as Stdb from "effect-spacetimedb"
declare const uri: string
declare const databaseName: string
declare const token: string
declare const userId: string
const AppErrors = Stdb.errors.namespace("App")({
UserMissing: Stdb.error({ userId: Stdb.string() }),
})
const Users = Stdb.StdbGroup.make("Users").add(
Stdb.StdbFn.reducer("user_require", {
params: Stdb.struct({ userId: Stdb.string() }),
errors: AppErrors,
}),
Stdb.StdbFn.procedure("user_get", {
params: Stdb.struct({ userId: Stdb.string() }),
returns: Stdb.option(Stdb.struct({ id: Stdb.string() })),
errors: AppErrors,
}),
)
const UserRoutes = Stdb.StdbHttpGroup.make("UserRoutes").add(
Stdb.StdbHttp.post("user_token_rotate", "/users/token/rotate", {
request: Stdb.struct({ userId: Stdb.string() }).schema,
response: Stdb.struct({ token: Stdb.string() }).schema,
errors: AppErrors,
}),
)
const Module = Stdb.StdbModule.make("app").add(Users).add(UserRoutes)
const Example = Stdb.project(Module.spec)
const rpcClient = Example.client.http.make({ uri, databaseName, token })
const requireUser = rpcClient.reducers.user_require({ userId }).pipe(
Effect.catchTag("AppUserMissing", (error) =>
Effect.log(`missing ${error.userId}`),
),
)
const findUser = rpcClient.procedures.user_get({ userId }).pipe(
Effect.catchTag("AppUserMissing", () => Effect.succeed(undefined)),
)
const api = Stdb.toHttpApi(Module.spec)
const rotateToken = Effect.gen(function* () {
const client = yield* HttpApiClient.make(api, {
baseUrl: Stdb.httpApiBaseUrl({ uri, databaseName }),
transformClient: HttpClient.mapRequest(
HttpClientRequest.bearerToken(token),
),
})
return yield* client.UserRoutes.user_token_rotate({
payload: { userId },
}).pipe(
Effect.map((response) => response.token),
Effect.catchTag("AppUserMissing", () => Effect.succeed(undefined)),
)
})

Use .raw(...) only when you need remote rejection metadata in addition to the decoded declared error.

Subscription and stream APIs report subscription, transport, invalidation, and row-decode failures. They do not carry declared domain errors because declared errors are emitted by reducer, procedure, and typed HTTP route calls.

Declared errors are Effect failures. Defects remain defects, and plain host rejection strings are kept separate from decoded domain failures.