fix(desktop): classify ENOENT spawn failures as PRECONDITION_FAILED, not 500

Merged
SA
3mo ago
+16-5

Problem

Triggering "Open in " for an editor whose CLI launcher is not installed (or not on PATH) reports to Sentry as an INTERNAL_SERVER_ERROR on external.openInApp (DESKTOP-QZ, 10 events on 1.20.2). A missing external application is an expected user-environment condition, not a bug — the guard and its user-facing message are correct, only the classification is wrong.

Root cause

spawnAsync in apps/desktop/src/lib/trpc/routers/external/helpers.ts wraps every child.on("error") into a plain new Error(...), dropping the ENOENT code. The tRPC boundary then defaults the plain error to INTERNAL_SERVER_ERROR, which the dumb 500=report Sentry middleware (apps/desktop/src/lib/trpc/index.ts) always captures.

Fix

In the shared spawnAsync error handler (used by both external.openInApp and external.openFileInEditor via openPathInApp), when the incoming spawn error has code === "ENOENT", reject with new TRPCError({ code: "PRECONDITION_FAILED", cause: { kind: "APP_NOT_INSTALLED", command } }) carrying the exact same message string. Non-ENOENT spawn failures (EACCES, crashes, non-zero exits) keep the plain-Error path and continue reporting as before.

The renderer surfaces this error via toast.error(...error.message...) in every caller (OpenInMenuButton, V2OpenInMenuButton, ClickablePath, usePathActions), and the message is unchanged, so the user-visible toast is identical. No Sentry-side filtering — classification happens at the throw site per the PR #6164 contract.

Verification

$ bunx biome check apps/desktop/src/lib/trpc/routers/external/helpers.ts
Checked 1 file in 17ms. No fixes applied.

$ cd apps/desktop && bun run typecheck
typecheck exit=0

$ bun test src/lib/trpc/routers/external/helpers.test.ts
 113 pass
 0 fail
 120 expect() calls
Ran 113 tests across 1 file. [74.00ms]

Refs DESKTOP-QZ

https://claude.ai/code/session_0968498a-7639-4840-b859-e8aab9fa335f


Summary by cubic

Classifies ENOENT spawn failures from “Open in ” as PRECONDITION_FAILED instead of INTERNAL_SERVER_ERROR, so expected “app not installed/not on PATH” cases no longer get reported to Sentry. Previously we wrapped spawn errors as plain Error, lost the ENOENT code, and tRPC returned a 500; now ENOENT throws a TRPCError with PRECONDITION_FAILED. User-facing toast text is unchanged; non-ENOENT errors behave as before.

Review notes

  • Change is isolated to spawnAsync in apps/desktop/src/lib/trpc/routers/external/helpers.ts, shared by external.openInApp and external.openFileInEditor.
  • For ENOENT only, throw new TRPCError({ code: "PRECONDITION_FAILED", cause: { kind: "APP_NOT_INSTALLED", command } }) from @trpc/server; other spawn errors keep the existing path.
  • No API or UI changes; no rollout or migration required. Refs DESKTOP-QZ.

Summary by CodeRabbit

  • Bug Fixes
    • Improved error handling when launching applications that aren’t installed.
    • Displays a clearer installation-related error instead of a generic failure.
    • Preserves existing handling for other application launch errors.