Plan for removing NamedError as connective tissue while keeping public
wire contracts stable.
Domain/service error
Schema.TaggedErrorClass
- catchable with catchTag / catchTags
- appears in service method error type
- no HTTP status
- no toObject()
HTTP public error
Schema.ErrorClass / TaggedErrorClass with httpApiStatus
- endpoint-declared public contract
- owns legacy { name, data } only when that is the SDK wire shape
CLI/user rendering
FormatError and small format helpers
- converts domain errors to text
- preserves useful structured fields
Session/model-visible error
first-class session/message error schema or helper
- owns { name, data } event/message shape
- not a service error class
The important rule: a service error should not also be the HTTP body, CLI formatter, and session event body. Each seam adapts the error into the shape it owns.
Before:
export const ModelNotFoundError = NamedError.create("ProviderModelNotFoundError", {
providerID: ProviderID,
modelID: ModelID,
suggestions: Schema.optional(Schema.Array(Schema.String)),
})
Problems:
Effect.fn made it behave like a defect unless a
compatibility bridge caught it.400..data.*, which couples them to the legacy { name, data }
wire shape.After:
export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundError>()("ProviderModelNotFoundError", {
providerID: ProviderID,
modelID: ModelID,
suggestions: Schema.optional(Schema.Array(Schema.String)),
cause: Schema.optional(Schema.Defect),
}) {}
export interface Interface {
readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect<Model, ModelNotFoundError>
}
Boundary adapters:
CLI
└─ FormatError sees _tag ProviderModelNotFoundError -> nice text
Session prompt
└─ catch ModelNotFoundError -> publish Session.Event.Error as message/session wire shape
HTTP route
└─ catch ModelNotFoundError -> declared BadRequest public API error when the endpoint needs it
HTTP middleware
└─ no Provider.ModelNotFoundError knowledge
Use EffectPromise.refineRejection(...) when a Promise boundary can reject
with many unknown values, but only one or two rejection classes are expected
domain failures. Unknown rejections stay defects; the helper maps only known
rejection shapes to typed errors.
const language =
yield *
EffectPromise.refineRejection(
async () => loadFromProvider(),
(cause) => (cause instanceof NoSuchModelError ? new ModelNotFoundError({ providerID, modelID, cause }) : undefined),
)
Use this when the Promise can genuinely reject and most rejection values are
still defects for the current module. Use Effect.tryPromise({ try, catch })
when every rejection should become the same expected error type. Use
Effect.promise(...) only when rejection means a defect and you do not need
to refine known rejection classes.
Add helpers only when repeated call sites prove the seam is real.
Likely location: src/server/routes/instance/httpapi/errors.ts.
Purpose:
{ name, data } where neededhttpApiStatusGood helpers:
notFound(message)
badRequest(message)
unknown()
Avoid:
mapAnyDomainError(error)
That recreates the giant middleware mapper problem.
Likely location: near src/session/message-error.ts or a new narrow
module such as src/session/event-error.ts.
Purpose:
{ name, data } shape used by Session.Event.Error and
assistant message errorsnew NamedError.Unknown(...).toObject() call sitesGood helpers:
unknown(message)
agentNotFound(agent, available)
commandNotFound(command, available)
modelNotFound(error: Provider.ModelNotFoundError)
Likely location: src/cli/error.ts until repetition demands domain-local
format helpers.
Purpose:
{ name, data } shapes only while compatibility is neededSession.BusyError and map it at route boundaries.NamedError middleware branch once no route relies
on defect-wrapped legacy domain errors.Cause.pretty(cause)
and returns a safe 500 body.NamedError.create(...) Service ErrorsThese should become Schema.TaggedErrorClass when touched:
src/provider/provider.ts — ProviderInitError.src/storage/db.ts — database NotFoundError.src/mcp/index.ts — MCPFailed.src/skill/index.ts — SkillInvalidError,
SkillNameMismatchError.src/lsp/client.ts — LSPInitializeError.src/ide/index.ts — install errors.src/config/error.ts, src/config/config.ts,
src/config/markdown.ts — config errors. These already render well
in the CLI, so migrate carefully and preserve diagnostics.These are not ordinary service errors. They mostly build { name, data }
objects for model-visible/session-visible output.
new NamedError.Unknown(...).toObject() in
src/session/prompt.ts.new NamedError.Unknown(...).toObject() in config/skill/plugin
session event publishing.src/session/message-error.ts and src/session/message-v2.ts
away from NamedError.create(...) once the wire helper exists.NamedError instances.{ name, data } and
new _tag shapes.NamedError.create(...) domains move
to Schema.TaggedErrorClass.For each migrated error:
Schema.TaggedErrorClass.toObject() just for compatibility.