Surface webpack cache generation errors in build output#26546
Surface webpack cache generation errors in build output#26546frankmueller-msft wants to merge 9 commits intomicrosoft:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR makes fluid-build webpack incremental-cache failures actionable by surfacing the underlying exception in normal build output (not only behind DEBUG=fluid-build:task:error), helping engineers diagnose why done-file content generation failed.
Changes:
- Updates
WebpackTask.getDoneFileContent()error handling to include clearer trace text. - Emits a
console.warnwith the package name, config path, and the underlying error message when done-file content generation fails.
When WebpackTask.getDoneFileContent() fails to load a webpack config, re-throw with root cause details instead of silently returning undefined. The error propagates to markExecDone()'s existing catch block, producing one warning that includes the actual error message rather than the vague "unable to generate content" message. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ba8c29f to
903621e
Compare
alexvy86
left a comment
There was a problem hiding this comment.
Generally I think I agree with the direction (I think surfacing the errors is valuable) but with a couple of caveats:
- In the codepath for
markExecDonethe message in the warning will be misleading; right now it would log... unable to generate content for ...and with this change it would say... warning: unable to write ...which arguably was intended to point to errors thrown byawait writeFile(doneFileFullPath, content);inleafTask'smarkExecDone, but now would point readers to the wrong root cause. - Same thing in
checkLeafIsUpToDateinsideLeafWithDoneFileTask. Current message is"unable to generate done file expected content (getDoneFileContent returned undefined)", and with the new behavior it would change to"unable to read compare file: ..."which seems to correspond to a failure in a call aftergetDoneFileContentis called and returns a non-undefined result.
Personally I wouldn't object strongly to updating the error messages in the new places that would be logging the error, so they more accurately reflect what could have gone wrong. But I'd also check with @tylerbutler about the general architecture of this part of build-tools, in case he was involved with this part.
The catch blocks in these methods blamed only file I/O (write/read) but getDoneFileContent() can also throw, making the messages inaccurate. Updated to "unable to generate or write/read" to reflect both failure modes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
frankmueller-msft
left a comment
There was a problem hiding this comment.
Good catches @alexvy86 — you're right that both messages were misleading. I've pushed a fix:
markExecDonecatch:"unable to write ..."→"unable to generate or write done file ..."checkLeafIsUpToDatecatch:"unable to read compare file: ..."→"unable to generate or read compare file: ..."
Both catch blocks can now be entered from either getDoneFileContent() throwing or the subsequent I/O operation failing, so the messages reflect both failure modes.
| throw new Error( | ||
| `failed to generate incremental build cache for ${this.configFileFullPath}: ${e instanceof Error ? e.message : e}`, | ||
| ); |
There was a problem hiding this comment.
Throwing a fresh Error here drops the original stack trace, which can make webpack config/load failures harder to diagnose (the message alone often isn’t enough). Consider preserving the original error via new Error(msg, { cause: e }) (when e is an Error) or rethrowing the original after adding context so the stack is retained.
| throw new Error( | |
| `failed to generate incremental build cache for ${this.configFileFullPath}: ${e instanceof Error ? e.message : e}`, | |
| ); | |
| const message = `failed to generate incremental build cache for ${this.configFileFullPath}: ${ | |
| e instanceof Error ? e.message : e | |
| }`; | |
| if (e instanceof Error) { | |
| throw new Error(message, { cause: e }); | |
| } | |
| throw new Error(message); |
There was a problem hiding this comment.
Done — using new Error(message, { cause: e }) when e is an Error to preserve the original stack trace.
| this.traceError(`error generating done file content: ${e}`); | ||
| throw new Error( | ||
| `failed to generate incremental build cache for ${this.configFileFullPath}: ${e instanceof Error ? e.message : e}`, | ||
| ); |
There was a problem hiding this comment.
this.configFileFullPath is recomputed in the catch block even though it was already used in the try (and the getter reparses this.command). It would be more maintainable (and avoid any inconsistency) to compute it once into a local variable and reuse it for loadModule, traceError, and the thrown error message.
There was a problem hiding this comment.
Done — extracted configFileFullPath to a local configPath variable before the try block.
| this.traceError(`error generating done file content: ${e}`); | ||
| throw new Error( | ||
| `failed to generate incremental build cache for ${this.configFileFullPath}: ${e instanceof Error ? e.message : e}`, |
There was a problem hiding this comment.
The logging/message formatting uses ${e} / the raw non-Error thrown value. If something throws a plain object, this will typically render as [object Object], which isn’t actionable. Consider normalizing with String(e) (or similar) and, when e is an Error, including e.stack in the traced output so failures are diagnosable.
| this.traceError(`error generating done file content: ${e}`); | |
| throw new Error( | |
| `failed to generate incremental build cache for ${this.configFileFullPath}: ${e instanceof Error ? e.message : e}`, | |
| const errorInfo = | |
| e instanceof Error | |
| ? `${e.message}${e.stack ? `\n${e.stack}` : ""}` | |
| : String(e); | |
| this.traceError(`error generating done file content: ${errorInfo}`); | |
| throw new Error( | |
| `failed to generate incremental build cache for ${this.configFileFullPath}: ${ | |
| e instanceof Error ? e.message : String(e) | |
| }`, |
There was a problem hiding this comment.
Done — using String(e) for non-Error values and e.message for Error instances. The original stack trace is preserved via { cause: e } rather than inlining e.stack in the trace output.
…ting
- Use { cause: e } to preserve original stack trace
- Extract configFileFullPath to local variable to avoid recomputation
- Use String(e) for non-Error thrown values to avoid [object Object]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Error constructor's options parameter (cause) is not supported by the TypeScript lib at es2022 target. Preserve the original stack trace by assigning error.stack directly instead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Any thoughts on this PR @tylerbutler ? |
Code reviewFound 1 issue:
🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Good catch @tylerbutler — you're right, the stack mutation was inconsistent. Pushed a fix: now using |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The build-tools tsconfig inherits lib: ES2020 which doesn't include the ErrorOptions type. Use property assignment instead of constructor options to set the cause. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
WebpackTask.getDoneFileContent()to throw on error instead of silently returningundefinedmarkExecDone()'s existing catch block, which prints one warning that includes the actual error message (e.g., the missing module or config parse failure)"warning: unable to generate content for /path/to/done.log"(no root cause)"warning: unable to write /path/to/done.log\n error: failed to generate incremental build cache for webpack.config.cjs: Cannot find module '...'"(actionable)Contract change
The original code caught errors and returned
undefined, treating failure as "no content available." This changes it to throw, treating failure as an error condition. Both current call sites (markExecDoneandcheckLeafIsUpToDate) have try/catch blocks that handle thrown errors correctly, so behavior is preserved. However,getDoneFileContent()'s return type isPromise<string | undefined>, which signals thatundefinedis a valid return. Future callers should be aware thatWebpackTask's override may throw instead.Test plan
Build - build-toolspipeline passes (build + policy checks)🤖 Generated with Claude Code