fix: bubble real dbt show error instead of generic "Could not parse"

Closed
SA
5mo ago
+128-4

Closes #932

Summary

altimate-dbt execute --query "..." (which calls execDbtShow in packages/dbt-tools/src/dbt-cli.ts) swallows the real error from dbt show and surfaces a misleading generic message:

{
  "error": "Could not parse dbt show output in any format (JSON, heuristic, or plain text). Got 0 JSON lines.",
  "fix": "Run: altimate-dbt doctor"
}

But running dbt show directly produces a clear actionable error, e.g.:

Runtime Error: Failed to read package: No dbt_project.yml found at expected path dbt_packages/dbt_utils/dbt_project.yml

The real error never reaches the caller. Agents read "could not parse" as transient and retry alternate commands instead of bailing out — burns budget on a project that is structurally broken.

Root cause

packages/dbt-tools/src/dbt-cli.ts has two adjacent catch blocks in execDbtShow that swallow the run() rejection silently:

// ~line 224 — Tier 1 JSON attempt
try { const { stdout } = await run(args); lines = parseJsonLines(stdout) }
catch { lines = [] }            // ← discards err.stderr

// ~line 298 — Tier 3 plain-text fallback
try { ... await run(plainArgs) ... }
catch { /* fall through */ }    // ← also discards err.stderr

throw new Error("Could not parse dbt show output in any format (JSON, heuristic, or plain text). ...")

When execFile rejects with a non-zero exit, the error object carries .stderr and .stdout. Both catches discard them. The generic "Could not parse" message was designed for the case "dbt exited 0 but the output is unparseable" — but it currently fires for the "dbt actually crashed" case as well, conflating two very different failure modes.

Fix

  • Capture the execFile rejection on both tiers (primaryRunError, plainRunError).
  • Recover any partial JSON log lines from the failed run's stdout — dbt with --log-format json emits structured level: "error" events before exit.
  • Before the generic throw, call extractDbtError(...) which picks (in order): structured error event > primary stderr > plain stderr > exception message.
  • If extractDbtError returns anything, surface it as dbt show failed: <real msg>. Fall through to the generic message only when both runs exited 0.

Net: ~45 lines in dbt-cli.ts (helper + 2 small guard sites), 6 new test cases.

Scope

Limited to execDbtShow. execDbtCompile and execDbtCompileInline share the same catch { lines = [] } pattern but have additional fallback paths (manifest.json for compile, no-args plain-text retry for inline) that reduce caller impact. Worth addressing in a follow-up PR if telemetry shows masking there too — kept out of this PR to keep the diff focused.

Test plan

  • bun test test/dbt-cli.test.ts → 24/24 pass (18 pre-existing + 6 new)
  • Generic "Could not parse" message still surfaces when dbt show exits 0 but emits unparseable output (regression guard preserved)
  • Real stderr from a crashing dbt show surfaces in the thrown error
  • Structured level: "error" event preferred over raw stderr when both present
  • spawn ENOENT (no dbt binary) surfaces clearly
  • Reviewer: smoke against a real project with a corrupted dbt_packages/<pkg>/dbt_project.yml; expect to see the real "Failed to read package" message, not "Could not parse"

Links

  • Issue: #932
  • Catalog entry: 03-issues-and-fixes.md Issue #14
  • Discovered while investigating the auto-deps clobber on a sister repo (altimate-dbt-integration#120); the masked error happened to be a corrupted dbt_packages/ fixture

Summary by CodeRabbit

  • Bug Fixes
    • dbt error messages now display actual error details instead of generic fallback messages
    • Enhanced error reporting to prioritize structured error information from dbt logs when available
    • Improved error capture and synthesis across execution modes, providing more accurate failure diagnostics