skip to content

In Ruby, after system or backticks runs a command, how do you check its exit status with $? and Process::Status?

level: middleimportance: must knowfreq 58%

answer

  1. the last child's status object
  2. thread-local, nil before any child
  3. exitstatus and success?
  4. signaled? and termsig
  5. $CHILD_STATUS via English

basics

~10 s

$? holds a Process::Status for the most recently exited child in the current thread. Read $?.success? or $?.exitstatus for a normal exit, and $?.signaled? with $?.termsig when a signal killed the process.

solid answer

~40 s

Each call that waits for a child (`system`, backticks, `Process.wait`) stores a `Process::Status` in `$?`, which is **thread-local** and `nil` until a child has exited; `Process.last_status` and `$CHILD_STATUS` (after `require "English"`) are the same value. `exitstatus` gives the exit code, but only if the process **exited**: it is `nil` when a signal killed it. `success?` is `true` for exit code zero, `false` for any other exit code and `nil` if the process did not exit. `signaled?` and `termsig` tell you which signal ended it, and `pid` identifies the child. Backticks never raise on failure, so a caller that skips `$?.success?` silently accepts partial output. With `system`, `false` means a non-zero exit, `nil` means it never ran, and `exception: true` turns both into exceptions.

code

ruby · 15 lines
ruby
out = `ffprobe -v error -show_format upload.mov`
unless $?.success?
  if $?.signaled?
    warn "ffprobe killed by signal #{$?.termsig}"
  else
    warn "ffprobe failed with exit #{$?.exitstatus}"
  end
end

ok = system("ffmpeg", "-i", "upload.mov", "out.mp4")
case ok
when true  then :done
when false then :failed   # ran, exited non-zero
when nil   then :missing  # could not execute
end

go deeper

for a junior

Recall that $? holds the last child's status and that $?.success? and $?.exitstatus tell you whether a command worked.

for a middle

Explain Process::Status in detail: exited?, exitstatus returning nil for signalled children, signaled? with termsig, and $? being thread-local.

for a senior

Show production habits: check every backtick call, log exit code and signal separately, and use exception: true or a wrapper so failures cannot be ignored.

for a principal

Define how external command failures surface across services, which statuses retry, which alert, and make the wrapper enforce it rather than each caller.

## Where the status lives When a child process ends, the operating system keeps a small **wait status**: whether the child exited normally and with which code, or which signal terminated it. Ruby wraps that in a **`Process::Status`** object and stores it in the global **`$?`**. - `$?` is set by the calls that wait for a child: `system`, backticks and `%x`, `Process.wait` and friends. - It is **thread-local**: each thread sees the status of the last child *it* waited for. - It starts as `nil`, before any child has been reaped in that thread. - `Process.last_status` returns the same object, and `require "English"` adds the readable alias `$CHILD_STATUS`. ## Reading a Process::Status | Method | Returns | |---|---| | `exited?` | `true` if the child exited normally | | `exitstatus` | the exit code (low 8 bits), or `nil` if it did not exit | | `success?` | `true` for exit code 0, `false` for another code, `nil` if it did not exit | | `signaled?` | `true` if a signal terminated it | | `termsig` | the number of that signal | | `pid` | the child's process id | | `to_i` | the raw wait status bits | Two return values deserve attention. `exitstatus` is `nil`, not `0` or `1`, when the child was killed by a signal, so code like `exit $?.exitstatus` raises `TypeError` in exactly the case that matters. And `success?` is `nil` rather than `false` for a signalled child, which is still falsy but tells you there was no exit code at all. ## Checking after each kind of call 1. **`system`** already summarises the status: `true`, `false` (non-zero exit) or `nil` (could not execute). `$?` gives the detail: a missing program is typically reported with exit status 127. 2. **Backticks** return the output string whatever happened, so the only failure signal is `$?`. Always follow them with `$?.success?`, or you will process truncated output as if it were complete. 3. **`spawn`** returns a pid; `$?` is set only when you call `Process.wait(pid)`. 4. **`Open3`** methods return the status object directly as part of their result; use that return value in preference to `$?`. ## Raising instead of checking `system(..., exception: true)` saves the manual check: - a non-zero exit raises `RuntimeError` with a message naming the exit status and the command; - a program that cannot be started raises a `SystemCallError`, for example `Errno::ENOENT`. That makes a failed step impossible to ignore by accident. ## A signalled child When a transcoding child is killed by the out-of-memory killer or by an operator's `kill`, it never exits normally. `$?.exited?` is `false`, `$?.signaled?` is `true`, and `$?.termsig` holds the signal number, for example 9 for KILL. Logging `termsig` as well as `exitstatus` is what distinguishes "ffmpeg rejected the file" from "ffmpeg was killed". ## Turning a status into a decision A background worker usually maps each outcome to a different action: | Outcome | How to detect it | Typical action | |---|---|---| | success | `success?` is `true` | mark the job done | | tool rejected the input | `exited?` with a non-zero `exitstatus` | fail the job, keep stderr | | killed by a signal | `signaled?` and `termsig` | retry later; the input may be fine | | could not start | `system` returned `nil`, or `Errno::ENOENT` raised | alert: the deployment is broken | Keeping the four apart is what makes a failed command actionable instead of a generic "it broke". ## Pitfalls - Reading `$?` in another thread than the one that ran the command: it is thread-local, so you see a different or `nil` status. - Reading it after a second command: `$?` always reflects the **latest** child. - Treating `exitstatus` as always an Integer. - Ignoring `nil` from `system`: the program never ran.

  • In Ruby, why can exit($?.exitstatus) raise TypeError after running a command?
    If a signal terminated the child, it never exited normally, so `exitstatus` returns `nil`, and `exit(nil)` raises `TypeError`. Check `$?.exited?` first, or map a signalled child to your own code using `$?.termsig`.
  • In Ruby, why might $? not reflect a command you just ran?
    `$?` is thread-local and always holds the most recently reaped child in the current thread. A command run in another thread, or a second command run in between, leaves you looking at a different status. Capture the status right after the call, or use an API such as `Open3.capture3` that returns it.

saying these in an interview costs you the question

  • $? is shared by every thread in the process.
  • Backticks raise an exception when the command exits non-zero.
  • exitstatus is always an Integer after a command finishes.
  • system returning nil means the command exited with status zero.
  • $? is set as soon as spawn returns.