fix: bubble real dbt show error instead of generic "Could not parse"
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
execFilerejection on both tiers (primaryRunError,plainRunError). - Recover any partial JSON log lines from the failed run's
stdout— dbt with--log-format jsonemits structuredlevel: "error"events before exit. - Before the generic throw, call
extractDbtError(...)which picks (in order): structured error event > primary stderr > plain stderr > exception message. - If
extractDbtErrorreturns anything, surface it asdbt 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 showexits 0 but emits unparseable output (regression guard preserved) - Real stderr from a crashing
dbt showsurfaces 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.mdIssue #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