Skip to content
Shell & TerminalDeep Dive Published Updated 8 min readViews unavailable

Nushell External Failures: Capture Streams and Preserve Exit Codes

Handle Nushell subprocess failures deliberately with try, complete, stderr routing, exit-code checks, and cleanup that preserves useful diagnostics.

Nushell’s built-in commands and external programs meet at a boundary with different data and failure models. Nu commands exchange pipeline values and raise Nushell errors. External programs communicate through byte-oriented stdout and stderr streams and an integer exit code. Nu integrates that process status into script error handling, which is safer than silently ignoring a failed command, but it means a script must choose deliberately when failure is expected and how much output it needs to retain.

The most important operational rule is that a non-zero external exit status is an error in a script. A later line may not execute, and an external failure inside a pipeline can fail the overall pipeline. That behavior prevents a downstream formatter from making a broken upstream command look successful. When a failure is an expected branch, capture it explicitly instead of globally weakening error behavior.

Separate stdout, stderr, and the exit status

An external command’s stdout is normally connected to a Nu pipeline when the command appears before |. Stderr is not redirected by default, so diagnostic text remains visible to the terminal. This separation is useful: data can flow through a pipeline while errors remain visible. It also means that redirecting stderr into a pipeline changes the meaning of that pipeline and can make diagnostics look like ordinary data.

^tool --format json | from json

This is appropriate only if tool documents JSON on stdout and exits successfully for the expected input. A parseable-looking response does not prove the command succeeded. If the external exits non-zero, Nu reports a failure; the pipeline’s rightmost failing external determines the status reported in the documented pipeline behavior. Do not infer success solely because a later command produced a value.

$env.LAST_EXIT_CODE exposes the most recently completed external status for interactive inspection. It is a mutable piece of ambient state, not a durable result object. Another external command can replace it. Read it immediately when using it diagnostically, and prefer capturing the status alongside stdout and stderr in automation.

Use try when failure is an exception path

try handles Nu errors raised by its block, including failures from external commands. A catch closure receives an error record, and for external command errors the documented record includes an exit_code field. This fits workflows where success returns a normal pipeline value but failure should be translated, reported, or allowed to propagate.

let status = try {
    ^false | lines
    'command succeeded'
} catch {|err|
    $"external command failed with exit code ($err.exit_code)"
}

$status

This example demonstrates the error shape rather than a useful production command. A real handler should include context such as the operation name and sanitized arguments, while avoiding secrets. Do not catch every error only to print a generic message and continue: that turns an actionable failure into an apparently successful run. Either handle a clearly expected condition or rethrow/create an error that preserves why the operation failed.

The finally block is for cleanup that must run whether the try block succeeds, a catch handles the error, or an unhandled error propagates. Nu’s documentation specifies that its result is discarded; it does not replace the value returned from the try or catch branch. Use it for releasing a temporary resource or emitting a bounded cleanup diagnostic, not for computing the main result.

try {
    ^tool --write temporary-output.json
} catch {|err|
    error make { msg: $"tool failed with exit code ($err.exit_code)" }
} finally {
    print 'operation scope finished'
}

The cleanup example is intentionally not deleting a file: cleanup must target a resource the script itself created, and must verify that target before removal. finally also runs when control leaves through return, break, or continue. If the process exits with Nu’s aborting exit path, the documented exception is that finally does not run; do not make abrupt process termination your resource-management strategy.

Use complete when output and status are all part of the result

For subprocess orchestration, a record containing stdout, stderr, and exit_code is often easier to reason about than ambient error state. The complete command waits for an external process and returns those three fields together. When stderr must be captured even though a non-zero status would ordinarily stop the pipeline, use the documented do -i { ... } | complete pattern.

let result = do -i {
    ^git status --porcelain=v1
} | complete

if $result.exit_code == 0 {
    $result.stdout | lines
} else {
    print -e $result.stderr
    error make {
        msg: $"git status failed with exit code ($result.exit_code)"
    }
}

The code keeps the status attached to the output and routes stderr only after checking the result. This makes it possible to distinguish an empty successful response from a command that emitted no stdout because it failed. complete returns text for the byte streams, not parsed records; decode stdout only after validating the exit status and the command’s output contract.

There is a tradeoff between try and complete. Use try when an error should participate in Nu’s normal error propagation and when the external output is not needed after failure. Use complete when the caller needs to inspect output, diagnostics, and status as one value. A wrapper can use complete, map known exit codes to domain results, and raise a new Nu error for all unhandled cases. That gives callers a stable interface without discarding the actual subprocess evidence.

Avoid a broad do -i around an entire script. It suppresses the normal failure behavior for work that may not be expected to fail. Restrict it to the smallest external invocation whose non-zero result is being captured, and immediately inspect the returned exit_code. Otherwise a later command may succeed and obscure the earlier failure.

Route streams only when you intend to change their role

Nushell provides explicit redirections for stdout, stderr, and both streams. e>| routes stderr into the next pipeline command; e> file writes stderr to a file. Combined redirection is also available. These operations are useful for collecting diagnostics, but the stream is still bytes. If stderr contains progress messages mixed with user data, treating it as a structured input can corrupt the pipeline.

^tool --version e> tool-diagnostics.log

File redirection persists outside the Nu pipeline and may overwrite a file depending on the operator used. Choose append versus replace intentionally, use a controlled path, and protect sensitive diagnostic output. If the command can fail and the details must be retained, prefer complete rather than assuming an stderr pipe will preserve output from a failing pipeline. The Nushell guide specifically warns that a failing external can cause redirected pipeline output to be lost; collecting the streams and status together avoids that ambiguity.

ignore is another intentional policy, not a generic error fix. It discards output and can ignore a non-zero status. Use it only when the result is irrelevant by design, and use its error-reporting option when a failure must remain visible. For operations such as cleanup where “already absent” is acceptable but permission errors are not, inspect and classify the specific status rather than ignoring every outcome.

Build wrappers around explicit contracts

A reusable Nu wrapper should document its external dependency, expected input, stdout format, stderr handling, and accepted exit statuses. Quote arguments according to Nu’s parser and the external program’s argument contract; do not concatenate a command line into one string and pass it to a shell. Calling an external with the ^ prefix makes the external boundary explicit when Nu also has a command of the same name.

For a retrying operation, capture status per attempt and retry only documented transient failures. Do not retry validation errors, authentication failures, or arbitrary non-zero codes without evidence. Place a bounded attempt count and delay around the operation, preserve the last stderr message, and make the final result fail if all attempts are exhausted. Logs should contain enough context to diagnose the failure but should not disclose credentials or private payloads.

For a pipeline, decide whether each stage is required to succeed. Nushell’s documented pipefail behavior is currently an experimental option that can be disabled at startup; changing that option alters a global failure contract. Prefer a local handler or a version-controlled, tested configuration over a hidden per-user toggle. A script should record the Nushell version and relevant experimental settings when those settings affect its correctness.

Validate the error path, not only the happy path

Create a harmless test command that emits known stdout and stderr and exits with a chosen status. Confirm that ordinary execution fails where expected, try receives the documented error record, and complete captures the streams and numeric status together. Test success with empty output as well as failure with empty stdout; otherwise an empty result can be misclassified.

Also test what happens in the middle of a multi-stage pipeline, since an earlier external failure may stop downstream work. Verify whether stderr is displayed, redirected, or captured; ensure that temporary output files have clear ownership; and confirm that a handler does not accidentally continue after an unrecognized status. Use disposable inputs and commands with no side effects in tests.

Nushell makes process failure visible by default. Preserve that advantage. Catch only what the caller can handle, capture complete process evidence when diagnosing is part of the interface, and convert unexpected failures back into a clear error. The result is an automation boundary whose status, data, and diagnostics can be audited independently instead of relying on whatever the last command happened to leave in the environment.

Related:

Sources:

Comments