Errors
mastra-effect bridges Mastra failures into a single Effect error, MastraError, whose reason field is a tagged union of *Error reason classes (for example AgentStreamFailedError, ToolExecutionFailedError). Use Effect.catchTag("MastraError", …) then Match.tag on reason — not coarse domain bags or top-level Mastra id strings.
Shape
export class MastraError extends Data.TaggedError("MastraError")<{
readonly reason: MastraErrorReason
}> {}
// reason classes — _tag is PascalCase *Error name (Effect style)
export class AgentStreamFailedError extends Data.TaggedError("AgentStreamFailedError")<MastraReasonFields> {}
type MastraReasonFields = {
readonly message: string
readonly domain: string // Mastra ErrorDomain
readonly category: string // Mastra ErrorCategory
readonly id: string // Mastra id, e.g. "AGENT_STREAM_FAILED"
readonly details?: Readonly<Record<string, unknown>>
readonly cause?: unknown
}Bridging
fromUnknown converts native @mastra/core MastraError instances (and a few named subclasses) into MastraError wrapping the matching reason:
import { fromUnknown } from "mastra-effect/Error"
import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error"
import { Effect, Match } from "effect"
const handleError = (cause: unknown) =>
Match.value(fromUnknown(cause)).pipe(
Match.tag("MastraError", (err) =>
Match.value(err.reason).pipe(
Match.tag("AgentStreamFailedError", (r) =>
Effect.log(`Agent stream failed (${r.domain}): ${r.message}`),
),
Match.tag("ToolExecutionFailedError", (r) =>
Effect.log(`Tool failed: ${JSON.stringify(r.details)}`),
),
Match.orElse((r) => Effect.log(`Other: ${r._tag}`)),
),
),
Match.orElse(() => Effect.log("Not a Mastra error")),
)
// Native Mastra errors from agent.stream / generate are bridged automatically:
const native = new MastraError({
id: "AGENT_STREAM_FAILED",
domain: ErrorDomain.AGENT,
category: ErrorCategory.SYSTEM,
text: "provider timeout",
})
Effect.runPromise(handleError(native))With Effect.catchTag:
Effect.catchTag("MastraError", (err) =>
Match.value(err.reason).pipe(
Match.tag("AgentStreamFailedError", (r) => /* r.message, r.details, r.id */),
Match.orElse(() => …),
),
)When Mastra throws a MastraError, fromUnknown preserves message, domain, category, details, and cause on the reason. The Mastra id string is kept on reason.id for logging and parity with upstream.
details typing
Mastra's native MastraError uses one class plus id / domain / category / optional details. details is not typed per id upstream — different ids use different keys. For example, TOOL_EXECUTION_FAILED may include { errorMessage, model } while AGENT_STREAM_FAILED may carry { threadId }. Generated reasons share MastraReasonFields; read reason.details?.… when you know the id.
Exports
| Export | Role |
|---|---|
fromUnknown | Bridge unknown/native errors at boundaries |
MastraError | Top-level Effect error with reason union |
MastraErrorReason | Generated reasons + UnknownMastraError |
reasonsById / mastraErrorsById | Lookup table: Mastra id → reason constructor |
ErrorDomain, ErrorCategory | Re-exported from @mastra/core/error |
ClientToolRequiredError | Client-fulfilled tool invoked on server |
ToolUI.Error | Tool UI encode/decode/runtime failures (ours) |
UnknownMastraError | Fallback reason when no Mastra id matches |
ClientToolRequiredError and ToolUI.Error remain separate top-level errors — not Mastra reasons.
All ~315 Mastra error ids from @mastra/core are codegen'd into src/errors/generated.ts as *Error reason classes. Regenerate after upgrading Mastra:
pnpm --filter mastra-effect run generate:errors