Skip to content

Error Taxonomy

All workflow errors are represented as AppError — a discriminated union on the type field defined in packages/application-contract/src/index.ts. This gives every layer a consistent, typed error vocabulary.

IPC or bridge-level failure. Only transport adapters create these.

{ type: "transport"; message: string; reason: TransportErrorReason; retryable: boolean }

TransportErrorReason is "ipc-disconnected" | "serialization" | "host-crash" | "timeout".

Created by the desktop renderer client when tRPC subscriptions fail, time out, or disconnect.

The workflow was aborted via AbortSignal.

{ type: "cancelled"; message: string }

Created by handlers or transport adapters when signal.aborted is detected.

Domain validation failure. Carries structured issues for display.

{ type: "validation"; message: string; issues: AppValidationIssue[] }

Created by application-layer handlers when input or domain state fails validation rules.

A required resource does not exist.

{ type: "not-found"; message: string; resource: "connection" | "course" | "group-set" | "assignment" | "repository" | "file" }

Created by handlers when a lookup returns nothing (e.g. loading a course that was deleted).

A write or identity collision.

{ type: "conflict"; message: string; resource: "course" | "connection" | "group-set" | "assignment" | "repository" | "file"; reason: string }

Created by handlers when an operation would violate uniqueness or consistency constraints.

LMS, Git, or subprocess adapter failure.

{ type: "provider"; message: string; provider: LmsProviderKind | GitProviderKind | "git"; operation: string; retryable: boolean }

Created by handlers when an external service call fails (API error, authentication failure, rate limit).

Settings, course, or user-file storage failure.

{ type: "persistence"; message: string; operation: "read" | "write" | "decode" | "encode"; pathHint?: string }

Created at the storage boundary when file I/O or serialization fails.

Catch-all for unclassified errors.

{ type: "unexpected"; message: string; retryable: boolean }

Created when an error doesn’t fit any other category. Indicates a bug or unhandled edge case.

Each layer is responsible for creating specific error types:

Error typeCreated by
transportTransport adapters only (desktop renderer client)
cancelledTransport adapters or handlers
validationApplication-layer handlers
not-foundApplication-layer handlers
conflictApplication-layer handlers
providerApplication-layer handlers (normalizing adapter errors)
persistenceStorage ports / application-layer handlers
unexpectedAny layer (last resort)
createTransportAppError(reason, message, retryable?) // → transport AppError
createCancelledAppError(message?) // → cancelled AppError
isAppError(value) // → boolean type guard

Handlers throw AppError instances. The tRPC router catches them in emitFailure(), which:

  1. Checks if the error is already an AppError (passes through)
  2. Otherwise wraps it as unexpected
  3. Emits a { type: "failed", error } event on the subscription

The renderer client receives the failed event and rejects the run() promise with the AppError.

Errors bubble directly from the handler to the Commander error handler. No serialization or wrapping occurs — the AppError is thrown and caught as-is.

Same as CLI — errors propagate directly. The React UI catches them and displays appropriate feedback.