skip to content

In Go's os/exec, how do cmd.Run, cmd.Output and cmd.CombinedOutput differ in what they capture?

level: middleimportance: must knowfreq 58%

answer

  1. three methods, two streams
  2. what happens to a nil Stdout field
  3. one of them merges the two streams
  4. only one fills ExitError.Stderr
  5. both convenience methods buffer in memory

basics

~20 s

Run just waits for the child and sends its output wherever Cmd.Stdout and Cmd.Stderr point, discarding it if they are nil. Output returns stdout as bytes. CombinedOutput returns stdout and stderr interleaved in one byte slice.

solid answer

~50 s

All three run the command to completion and return a nil error only if it exited with status zero. They differ in where the child's two output streams go. `Run` writes them to whatever you assigned to `Cmd.Stdout` and `Cmd.Stderr` — an `os.File`, a `bytes.Buffer`, anything implementing `io.Writer` — and if a field is nil that stream is connected to the null device and lost. `Output` requires `Cmd.Stdout` to be nil and returns stdout as a `[]byte`; if `Cmd.Stderr` is also nil it additionally captures a bounded amount of stderr into the `Stderr` field of the returned `*exec.ExitError`. `CombinedOutput` requires both fields nil and points both streams at one buffer, so you get them interleaved with no way to tell them apart. Both convenience methods buffer everything in memory, which matters for a chatty child.

code

go · 11 lines
go
// 1. Streams to the parent's own terminal; nothing is returned.
cmd := exec.Command(bin, "status")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()

// 2. stdout as bytes; stderr rides along on the ExitError.
out, err := exec.Command(bin, "status").Output()

// 3. stdout and stderr interleaved in one slice.
both, err := exec.Command(bin, "status").CombinedOutput()

go deeper

for a junior

Know that Run gives you nothing back unless you set the writer fields, Output hands you stdout as bytes, and CombinedOutput hands you both streams merged. Recall that all three wait for the child to finish.

for a middle

Explain the mechanics: which fields each method requires to be nil, that a nil stream goes to the null device, and that only Output populates the Stderr field of the returned ExitError.

for a senior

Show judgment about output volume and parseability — when a buffering helper is a memory risk, and why a step whose output you parse should keep stderr on a separate path.

for a principal

Own the convention for how orchestrated tool output reaches your logs and your error messages, so a failure in any step is diagnosable from the artefacts alone.

## One process, three ways to collect its output An `exec.Cmd` has three stream fields — `Stdin`, `Stdout`, `Stderr` — plus methods that run it. Understanding the trio is mostly understanding what each method does to those fields. ### `Run` `Run` is exactly `Start` followed by `Wait`: it launches the process and blocks until it has exited. It touches nothing; the child's stdout goes to whatever `Cmd.Stdout` holds and its stderr to `Cmd.Stderr`. Each is an `io.Writer`, so you can point them at `os.Stdout` to let the child write straight through to your own terminal, at a `bytes.Buffer` to collect them separately, at a log writer, or at the same writer to merge them yourself. The important default: **if `Stdout` or `Stderr` is nil, that stream is connected to the null device** (`os.DevNull`). It is not inherited from the parent and it is not buffered anywhere — it is thrown away. A team that debugs a failing step by "just running it again with `Run`" and sees nothing is usually looking at this. ### `Output` `Output` is the convenience wrapper for "I want what it printed". It returns `([]byte, error)`. It insists that `Cmd.Stdout` be nil, because it needs to install its own buffer there; if you already set the field it refuses with an error saying stdout is already set. It has one extra kindness: if `Cmd.Stderr` is *also* nil, it captures a bounded slice of the child's stderr and attaches it to the `Stderr` field of the `*exec.ExitError` it returns on a nonzero exit. That is the only way that field is ever populated — `Run` and `CombinedOutput` leave it empty — and it is the reason `Output` is often the better choice for a step whose failure message you want to report. Because the capture is bounded, a program that prints megabytes of diagnostics will have the middle of it elided in that field. Do not treat it as a full log. ### `CombinedOutput` `CombinedOutput` requires **both** `Stdout` and `Stderr` to be nil and assigns the *same* buffer to both. The result is one `[]byte` holding whatever the child wrote to either stream, in the order the writes reached the pipe. This is what you want when the tool interleaves progress and errors and you want to show a human exactly what it printed. It is what you do **not** want when you intend to parse stdout as data, because a stray warning on stderr lands in the middle of your JSON. ### What they share - They all block until the child exits, so none of them is a way to stream output as it arrives. - They all return `nil` only if the process ran **and** exited zero. A nonzero exit is an `*exec.ExitError`; a failure to launch is a different error type entirely. - The two convenience methods accumulate output in memory with no ceiling of their own. For a child that can produce unbounded output, assign your own bounded or streaming writer to `Cmd.Stdout` and call `Run` instead. ## Choosing between them A useful rule for a program that orchestrates other tools: - **The child's output is data you will parse** — use `Output`, and leave `Stderr` nil so a failure still carries its message. - **The child's output is a log for a human** — use `CombinedOutput` and attach the whole slice to any error you return, or use `Run` with `Cmd.Stdout` and `Cmd.Stderr` pointed at your own logger so it appears live. - **The child is interactive or long-running** — use `Run` with real writers; the buffering methods will hold everything until the end. ## A worked shape ```go cmd := exec.Command(bin, "apply", "-f", "service.yaml") cmd.Dir = workdir out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("apply failed: %w: %s", err, out) } ``` The `%s` on a `[]byte` prints it as text, so the tool's own message travels with your error. That single line is the difference between a deploy failure a colleague can read at 3am and one that says only `exit status 1`.

  • What does cmd.Output() return if you have already assigned cmd.Stdout yourself?
    It refuses and returns an error stating that stdout is already set. `Output` needs to install its own buffer in that field, so it will not silently overwrite yours or duplicate the stream. `CombinedOutput` behaves the same way and additionally rejects a non-nil `Cmd.Stderr`.
  • Why is CombinedOutput a poor choice when the child emits JSON you intend to parse?
    Both streams write into one buffer, so any warning, progress line or deprecation notice the tool sends to stderr is interleaved with the JSON and breaks the decoder. Use `Output` so stdout stays clean, and let stderr be captured separately for the error message.
  • How would you cap the memory a chatty child can consume?
    Do not use the buffering helpers. Assign your own `io.Writer` to `Cmd.Stdout` — a `bytes.Buffer` wrapped in something that stops after N bytes, a rotating log writer, or a file — and call `Run`. `Output` and `CombinedOutput` grow their buffer for as long as the child keeps writing.

saying these in an interview costs you the question

  • Thinks a nil Cmd.Stdout is inherited from the parent
  • Expects Output to include stderr in its result
  • Believes CombinedOutput separates the two streams
  • Assumes these methods stream output as it arrives
  • Uses CombinedOutput and then parses the result as JSON