Skip to content

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 ​

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

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

ts
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 ​

ExportRole
fromUnknownBridge unknown/native errors at boundaries
MastraErrorTop-level Effect error with reason union
MastraErrorReasonGenerated reasons + UnknownMastraError
reasonsById / mastraErrorsByIdLookup table: Mastra id → reason constructor
ErrorDomain, ErrorCategoryRe-exported from @mastra/core/error
ClientToolRequiredErrorClient-fulfilled tool invoked on server
ToolUI.ErrorTool UI encode/decode/runtime failures (ours)
UnknownMastraErrorFallback 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:

bash
pnpm --filter mastra-effect run generate:errors