Skip to content

Introduce domain-scoped error types with DomainError interface#7143

Draft
ryancbahan wants to merge 1 commit intomainfrom
ryan/dedicated-error-types
Draft

Introduce domain-scoped error types with DomainError interface#7143
ryancbahan wants to merge 1 commit intomainfrom
ryan/dedicated-error-types

Conversation

@ryancbahan
Copy link
Copy Markdown
Contributor

@ryancbahan ryancbahan commented Apr 1, 2026

What

Replace generic AbortError throws with domain-scoped error types that carry structured data. Rendering moves to the display boundary (FatalError.tsx), not the throw site.

Why

Everything was AbortError. The validate command used string matching (message.startsWith('Validation errors in ')) to distinguish config validation from auth failures. Error constructors baked in rendering (ANSI, newlines, styled templates). Machine consumers (JSON output) had to strip formatting to get clean data.

How

DomainError<TCode, TDetails> interface in cli-kit/error.ts — shared contract: {code, details}. Each domain defines typed codes and a typed details shape.

Four domain error classes, each colocated with its model:

Error File Codes Details
ProjectError project.ts 'no-project-root' | 'no-app-configs' {directory}
ActiveConfigError active-config.ts 'config-not-found' {configName, directory}
AppConfigValidationError loader.ts 'schema-validation' {configPath, errors}
TomlFileError toml-file.ts 'toml-not-found' | 'toml-parse-error' {path, message}

All extend AbortError (framework renders them). Constructors pass a minimal debug string to super(), not a rendered message.

FatalError.tsx — rendering boundary. Checks isDomainError(), switches on code, renders structured data with React/Ink. No domain error carries pre-rendered display text.

Validate command — single try/catch, instanceof switching. No string matching. Auth/remote errors fall through. Multiple TOML errors reported (not just first).

app-context.ts — throws TomlFileError directly instead of wrapping in new AbortError(...).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

We found no new type declarations in this PR

Existing type declarations

packages/cli-kit/dist/public/node/error.d.ts
@@ -33,6 +33,15 @@ export declare abstract class FatalError extends Error {
      */
     constructor(message: TokenItem | OutputMessage, type: FatalErrorType, tryMessage?: TokenItem | OutputMessage | null, nextSteps?: TokenItem<InlineToken>[], customSections?: AlertCustomSection[]);
 }
+/**
+ * Shared interface for domain-scoped errors. Each domain model defines its own
+ * error class that extends AbortError and implements this interface.
+ *  is the discriminant,  carries domain-specific structured data.
+ */
+export interface DomainError<TCode extends string = string, TDetails extends Record<string, unknown> = Record<string, unknown>> {
+    readonly code: TCode;
+    readonly details: TDetails;
+}
 /**
  * An abort error is a fatal error that shouldn't be reported as a bug.
  * Those usually represent unexpected scenarios that we can't handle and that usually require some action from the developer.
packages/cli-kit/dist/public/node/toml/toml-file.d.ts
 import { JsonMapType } from './codec.js';
+import { AbortError, type DomainError } from '../error.js';
+export type TomlFileErrorCode = 'toml-not-found' | 'toml-parse-error';
 /**
  * An error on a TOML file — missing or malformed.
- * Extends Error so it can be thrown. Carries path and a clean message suitable for JSON output.
+ * Carries structured data; rendering happens at the display boundary.
  */
-export declare class TomlFileError extends Error {
-    readonly path: string;
-    constructor(path: string, message: string);
+export declare class TomlFileError extends AbortError implements DomainError<TomlFileErrorCode, {
+    path: string;
+    message: string;
+}> {
+    readonly code: TomlFileErrorCode;
+    readonly details: {
+        path: string;
+        message: string;
+    };
+    constructor(code: TomlFileErrorCode, details: {
+        path: string;
+        message: string;
+    });
 }
 /**
  * General-purpose TOML file abstraction.
  *
  * Provides a unified interface for reading, patching, removing keys from, and replacing
  * the content of TOML files on disk.
  *
  * - `read` populates content from disk
  * - `patch` does surgical WASM-based edits (preserves comments and formatting)
  * - `remove` deletes a key by dotted path (preserves comments and formatting)
  * - `replace` does a full re-serialization (comments and formatting are NOT preserved).
  * - `transformRaw` applies a function to the raw TOML string on disk.
  */
 export declare class TomlFile {
     /**
      * Read and parse a TOML file from disk. Throws {@link TomlFileError} if the file
      * doesn't exist or contains invalid TOML.
      *
      * @param path - Absolute path to the TOML file.
      * @returns A TomlFile instance with parsed content.
      */
     static read(path: string): Promise<TomlFile>;
     readonly path: string;
     content: JsonMapType;
     readonly errors: TomlFileError[];
     constructor(path: string, content: JsonMapType);
     /**
      * Surgically patch values in the TOML file, preserving comments and formatting.
      *
      * Accepts a nested object whose leaf values are set in the TOML. Intermediate tables are
      * created automatically. Setting a leaf to `undefined` removes it (use `remove()` for a
      * clearer API when deleting keys).
      *
      * @example
      * ```ts
      * await file.patch({build: {dev_store_url: 'my-store.myshopify.com'}})
      * await file.patch({application_url: 'https://example.com', auth: {redirect_urls: ['...']}})
      * ```
      */
     patch(changes: {
         [key: string]: unknown;
     }): Promise<void>;
     /**
      * Remove a key from the TOML file by dotted path, preserving comments and formatting.
      *
      * @param keyPath - Dotted key path to remove (e.g. 'build.include_config_on_deploy').
      * @example
      * ```ts
      * await file.remove('build.include_config_on_deploy')
      * ```
      */
     remove(keyPath: string): Promise<void>;
     /**
      * Replace the entire file content. The file is fully re-serialized — comments and formatting
      * are NOT preserved.
      *
      * @param content - The new content to write.
      * @example
      * ```ts
      * await file.replace({client_id: 'abc', name: 'My App'})
      * ```
      */
     replace(content: JsonMapType): Promise<void>;
     /**
      * Transform the raw TOML string on disk. Reads the file, applies the transform function
      * to the raw text, writes back, and re-parses to keep `content` in sync.
      *
      * Use this for text-level operations that can't be expressed as structured edits —
      * e.g. Injecting comments or positional insertion of keys in arrays-of-tables.
      * Subsequent `patch()` calls will preserve any comments added this way.
      *
      * @param transform - A function that receives the raw TOML string and returns the modified string.
      * @example
      * ```ts
      * await file.transformRaw((raw) => `# Header comment\n${raw}`)
      * ```
      */
     transformRaw(transform: (raw: string) => string): Promise<void>;
     private decode;
 }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant