chore(cli): rename FernCliError to TaskAbortSignal#14746
Conversation
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.
Tip: disable this comment in your organization's Code Review settings.
🌱 Seed Test SelectorSelect languages to run seed tests for:
How to use: Click the ⋯ menu above → "Edit" → check the boxes you want → click "Update comment". Tests will run automatically and snapshots will be committed to this PR. |
SDK Generation Benchmark ResultsComparing PR branch against latest nightly baseline on Full benchmark table (click to expand)
main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via |
2f457fe to
b7324ed
Compare
SDK Generation Benchmark ResultsComparing PR branch against latest nightly baseline on Full benchmark table (click to expand)
main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via |
b7324ed to
b06c68c
Compare
SDK Generation Benchmark ResultsComparing PR branch against latest nightly baseline on Full benchmark table (click to expand)
main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via |
b06c68c to
5842013
Compare
SDK Generation Benchmark ResultsComparing PR branch against latest nightly baseline on Full benchmark table (click to expand)
main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via |
5842013 to
06590df
Compare
SDK Generation Benchmark ResultsComparing PR branch against latest nightly baseline on Full benchmark table (click to expand)
main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via |
06590df to
8c1a873
Compare
SDK Generation Benchmark ResultsComparing PR branch against latest nightly baseline on Full benchmark table (click to expand)
main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via |
48cd7ac to
ddfbe75
Compare
SDK Generation Benchmark ResultsComparing PR branch against latest nightly baseline on Full benchmark table (click to expand)
main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via |
ddfbe75 to
5e8f14e
Compare
SDK Generation Benchmark ResultsComparing PR branch against latest nightly baseline on Full benchmark table (click to expand)
main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via |
Made-with: Cursor
5e8f14e to
32b33b9
Compare
SDK Generation Benchmark ResultsComparing PR branch against latest nightly baseline on Full benchmark table (click to expand)
main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via |
…entry routing (#14749) ## Description Introduces a structured `CliError` class in `@fern-api/task-context` with typed error codes and automatic Sentry routing. This replaces the ad-hoc `new Error(...)` pattern throughout the CLI with a system that categorizes errors into user-facing codes (e.g. `CONFIG_ERROR`, `AUTH_ERROR`) vs internal errors that should be reported to Sentry. It also simplifies error handling by removing redundant error classes and unifying the ones we already have. > **Recommended review approach:** commit-by-commit. Each commit is self-contained and builds on the previous one. ### Prerequisite PRs - #14746 — rename `FernCliError` to `TaskAbortSignal` (clears the naming space) - #14747 — make CLI v1 telemetry synchronous (so `failWithoutThrowing` can report errors) - #14748 — make CLI v2 telemetry synchronous (same, for CLI v2) ### Follow-up PRs - #14750 — temporarily disable Sentry for unclassified errors (safety net while migrating) - #14753 — migrate `@fern-api/cli` package (example of a package migration PR) - ~34 more package migration PRs (one per package, converting `new Error(...)` → `new CliError(...)` and adding error codes to `failAndThrow` and `failWithoutThrowing` calls) - #14752 — re-enable Sentry for unclassified errors (merge last, after all migrations) ## Design Decisions ### Two ways to trigger and track errors There are two paths through which errors are captured and reported: 1. **Throwing `CliError` directly.** Code anywhere in the CLI can `throw new CliError({ message, code })`. The top-level catch handler in each CLI entry point (CLI v1's `runCli` catch in `cli.ts`, CLI v2's `withContext` catch in `withContext.ts`) intercepts it, resolves the error code, and routes it to Sentry and/or PostHog. 2. **Calling `failAndThrow` / `failWithoutThrowing` on the task context.** These methods log the error, resolve the code (explicit override > `CliError.code` > fallback), and report to Sentry if the code is Sentry-reportable. `failAndThrow` then throws a `TaskAbortSignal` to unwind the stack; `failWithoutThrowing` marks the task as failed and returns. This is the preferred path when the caller needs control over what happens after the error — for example, to continue processing other tasks or to clean up resources before aborting. It also adapts naturally to the existing error-handling patterns already used throughout the codebase. Both paths converge on the same code-resolution logic (`resolveErrorCode`) and Sentry-routing rules (`shouldReportToSentry`), so tracking is consistent regardless of which path is used. ### Default behavior: unclassified errors are internal Any error that doesn't carry a `CliError.Code` (i.e. a plain `new Error(...)`) is treated as `INTERNAL_ERROR` at the top-level catch boundaries — and therefore reported to Sentry. The assumption is that if nobody explicitly categorized an error as user-facing, it's likely an internal bug. **Temporary exception:** the follow-up PR #14750 temporarily downgrades this so unclassified errors skip Sentry during the migration period, to avoid noise from the ~34 packages that haven't been migrated yet. Once all migrations land, #14752 re-enables it. ### Error code taxonomy Errors are classified into 12 typed codes: | Code | Sentry? | Description | |------|---------|-------------| | `INTERNAL_ERROR` | Yes | Unexpected bugs — should be investigated | | `RESOLUTION_ERROR` | Yes | Type/reference resolution failures (likely IR bugs) | | `IR_CONVERSION_ERROR` | Yes | IR generation failures | | `CONTAINER_ERROR` | Yes | Docker container failures | | `VERSION_ERROR` | Yes | Version parsing/compatibility issues | | `PARSE_ERROR` | No | Malformed user input (YAML, OpenAPI, etc.) | | `ENVIRONMENT_ERROR` | No | Missing env vars, wrong Node version, etc. | | `REFERENCE_ERROR` | No | Dangling references in user config | | `VALIDATION_ERROR` | No | Schema/rule validation failures | | `NETWORK_ERROR` | No | HTTP failures, timeouts | | `AUTH_ERROR` | No | Authentication/authorization issues | | `CONFIG_ERROR` | No | Invalid generators.yml, fern.config.json, etc. | Only the first 5 codes are Sentry-reportable — they indicate bugs in Fern itself. The rest are user-actionable and would just create noise in Sentry. ### Simplifying error handling by removing redundant classes - **Unified `CliError` across CLI v1 and v2.** CLI v2 had its own `CliError` class in `packages/cli/cli-v2/src/errors/CliError.ts`. This PR deletes it and makes both CLIs share the single `CliError` from `@fern-api/task-context`, ensuring consistent error codes and Sentry routing regardless of which CLI entry point is used. - **Existing error classes now extend `CliError`.** `ValidationError`, `SourcedValidationError`, and `KeyringUnavailableError` now extend `CliError` with their appropriate codes (`VALIDATION_ERROR` and `AUTH_ERROR`). This means `resolveErrorCode()` handles them automatically — no special-casing needed in every catch block. - **Removed `LoggableFernCliError`.** This was a wrapper that carried a log message alongside an error. With `CliError` now carrying a `message` field (it extends `Error`), the wrapper is redundant. All former `LoggableFernCliError` usages are replaced with `CliError`. ### `reportError` — single error reporting path (CLI v2) CLI v2's `withContext.ts` previously had separate `shouldReportToSentry` and `extractErrorCode` functions. These are consolidated into a single `reportError(context, error, options?)` function that: 1. Skips `TaskAbortSignal` (already logged) 2. Resolves the error code via `resolveErrorCode()` (explicit override > `CliError.code` > fallback to `INTERNAL_ERROR`) 3. Reports to Sentry if the code is in `SENTRY_REPORTABLE_CODES` 4. Always reports to PostHog with the error code as a property ### Sentry tags Error codes are now passed as Sentry tags (`errorCode`) on captured exceptions, making it possible to filter and alert on specific error categories in the Sentry dashboard. ## Commits (review in order) 1. **`add shared CliError class`** — introduces `CliError` in `@fern-api/task-context` with the code taxonomy, `shouldReportToSentry`, `resolveErrorCode`, and static factory methods 2. **`unify CLI v2 CliError`** — deletes CLI v2's local `CliError`, switches to shared one, introduces `reportError` in `withContext.ts` 3. **`clean up imports`** — mechanical import reordering for consistency 4. **`make error classes extend CliError`** — `ValidationError`, `SourcedValidationError`, `KeyringUnavailableError` now extend `CliError` 5. **`pass error code as Sentry tag`** — adds `errorCode` tag to `captureException` calls 6. **`remove LoggableFernCliError`** — replaces all usages with `CliError`, deletes the class ## Testing - [x] Updated test helpers and mocks for new `CliError` / `MockTaskContext` signatures - [x] Existing CLI v1 and v2 tests pass <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/fern-api/fern/pull/14749" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end -->

Description
Part of the CLI error classification effort — see #14749 for full context.
Renames
FernCliErrortoTaskAbortSignalto better reflect its purpose: it is a control-flow signal used to abort a task after an error has already been logged, not an error class itself. This clears the naming space for theCliErrorclass introduced in #14749.Changes Made
FernCliErrorclass toTaskAbortSignalacross all usagespackages/cli/task-context/src/FernCliError.ts, createdpackages/cli/task-context/src/TaskAbortSignal.tsimport { FernCliError }→import { TaskAbortSignal }andinstanceof FernCliError→instanceof TaskAbortSignalacross CLI v1, CLI v2, seed, and test helpersContext
The old
FernCliErrorwas thrown byfailAndThrow()purely as a sentinel to unwind the call stack — it carried no error metadata. Renaming it toTaskAbortSignalmakes this intent explicit and avoids confusion with the newCliErrorclass that carries typed error codes for Sentry routing.Testing