Error messages can be lost/truncated when tfx is run as a child process and fails
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 75/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- node.js, typescript
Research direction
The issue is in app/lib/errorhandler.ts's errLog function. Start by reading the Node.js documentation on process.exit and asynchronous I/O. Look at the existing code and the proposed fix. Run the reproduction script to see the bug, then modify errLog to set process.exitCode instead of calling process.exit. Test by running tfx as a child process and verifying the full error message appears in stderr.
Written by the indexing model from the issue text.
Description
Summary
When tfx fails and exits via the CLI's error handler, error output written to stderr can be lost or truncated. This is most noticeable when tfx is invoked as a child process by another tool (e.g. a build task, a CI pipeline step, or a wrapper script) that pipes/redirects its stdout/stderr.
Root cause
app/lib/errorhandler.ts's errLog() handler does:
export function errLog(arg: any): void {
trace.debug(arg?.stack);
trace.error(formatError(arg));
process.exit(-1);
}
trace.error() ultimately calls console.error(), which writes to process.stderr. When stderr is a TTY, this write is synchronous, so it's safe to call process.exit() immediately afterwards. However, when stderr is piped or redirected (which is exactly what happens when tfx is spawned as a child process with stdio: 'pipe'), writes to it become asynchronous.
Calling process.exit() immediately after an async write does not wait for that write to flush. Per the Node.js docs:
In most situations, it is not actually necessary to call
process.exit()explicitly... Node.js will exit by itself once the event loop no longer has any additional work to schedule. ... callingprocess.exit()will force the process to exit as quickly as possible even if there are still asynchronous operations pending that have not yet completed fully, including I/O operations toprocess.stdoutandprocess.stderr.
As a result, the last (and most important) line(s) of error output — the actual failure message — can be silently dropped or truncated before the parent process finishes reading tfx's stderr. This is especially confusing since the process still exits with a non-zero code, but the accompanying error message is missing, making the failure very hard to diagnose from CI logs.
Reproduction
- Invoke tfx from a parent process that pipes stdio, e.g. from Node:
const { spawnSync } = require('child_process'); const result = spawnSync('tfx', ['extension', 'isvalid', '--publisher', 'x', '--extension-id', 'y'], { stdio: 'pipe' }); console.log('stderr:', result.stderr.toString()); - Trigger a failure path in
tfx(e.g. invalid credentials, unreachable service, invalid arguments). - Observe that
stderrcaptured by the parent process is empty or missing the final error line, even thoughtfxexited with a non-zero exit code.
This is more likely to reproduce on larger error messages (e.g. formatted AggregateError output with multiple lines) and on Windows, where pipes have smaller buffers, but can happen with any redirected stderr.
Proposed fix
Stop calling process.exit() explicitly in errLog(). Instead, set process.exitCode = -1; and let Node.js exit naturally once the event loop drains. This is exactly what the successful command path already does today (app/app.ts's Bootstrap.begin().then(() => {}) never calls process.exit() either), so this makes the error path consistent with the success path and guarantees all buffered output is flushed before the process terminates.
export function errLog(arg: any): void {
trace.debug(arg?.stack);
trace.error(formatError(arg));
process.exitCode = -1;
}
Alternative fix
If explicitly terminating the process is still desired (e.g. to avoid waiting on unrelated open handles), an alternative is to deliberately drain stdout/stderr before exiting, only calling process.exit() once any pending writes have completed:
function flushAndExit(code: number): void {
const streams = [process.stdout, process.stderr];
let pending = 0;
const tryExit = () => {
if (pending <= 0) {
process.exit(code);
}
};
streams.forEach(stream => {
if (stream && stream.writableLength > 0) {
pending++;
stream.write("", () => {
pending--;
tryExit();
});
}
});
tryExit();
}
This preserves the current "hard exit" behavior while ensuring queued writes are flushed first. Note: we evaluated using the (unmaintained, 2013) exit npm package for this, but found it monkey-patches stream.write to a permanent no-op and registers a process.on('exit', ...) listener that forcibly re-exits — behavior that is unsafe for anything other than a true one-shot process, so we don't recommend it here.
Suggested resolution
A PR implementing the first ("stop calling process.exit() explicitly") option will be linked to this issue.
- Dominant language
- TypeScript
- Stars
- 386
- Forks
- 142
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 6
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from microsoft/tfs-cli
-
Area: tfx-cli DevOps enhancement
Difficulty 1/5 Under an hour Newbie friendliness 68/100
-
Area: tfx-cli triage
Difficulty 3/5 1-2 days Newbie friendliness 58/100
-
Area: tfx-cli triage
Difficulty 4/5 3-5 days Newbie friendliness 48/100
-
Area: tfx-cli triage
Difficulty 3/5 1-2 days Newbie friendliness 45/100
-
Area: tfx-cli triage
Difficulty 4/5 3-5 days Newbie friendliness 35/100
All issues in microsoft/tfs-cli
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
vercel-labs/just-bash#464 ·
-
looksLikeSlug() is ASCII-only, so non-Latin entity slugs (e.g. Korean) skip exact match and collapse Open
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
-
Difficulty 1/5 Under an hour Newbie friendliness 90/100
TanStack/tanstack.com#1293 ·