skip to content

In Ruby, how do exit, exit! and abort differ in exit status, SystemExit, ensure clauses and at_exit handlers?

level: middleimportance: must knowfreq 45%

answer

  1. one raises, one does not
  2. SystemExit is not a StandardError
  3. exit defaults to true, exit! to false
  4. abort writes to $stderr, status 1
  5. exit! skips ensure and at_exit

basics

~20 s

exit raises SystemExit (default status 0), so ensure clauses and at_exit handlers run and it can be rescued. abort prints its message to $stderr and raises SystemExit with status 1. exit! ends the process at once (default status 1), skipping ensure and at_exit.

solid answer

~30 s

`exit(status = true)` raises `SystemExit`; the stack unwinds, `ensure` clauses run, then `at_exit` handlers and finalizers run, and the status reaches the OS (`true` is 0, `false` is 1, an integer as given). Because it is an exception, `rescue SystemExit` can intercept it and cancel the exit. `abort(msg)` writes `msg` to `$stderr` and raises `SystemExit` with status 1, so cleanup still runs. `exit!(status = false)` calls the operating system's immediate exit: no unwinding, no `ensure`, no `at_exit`, no finalizers, and unflushed Ruby IO buffers can be lost. `Process.exit`, `Process.exit!` and `Process.abort` behave the same.

code

ruby · 8 lines
ruby
begin
  exit 3
rescue SystemExit => e
  p [e.status, e.success?]   # => [3, false]
end
puts "still running"        # printed: the exit was cancelled

abort "rotation failed"      # stderr: rotation failed; status 1

go deeper

for a junior

Recall that exit ends the script with status 0 by default and abort prints a message and exits with 1. Know that non-zero means failure to the shell.

for a middle

Explain that exit and abort raise SystemExit, so ensure and at_exit run, while exit! skips both. State the defaults: exit true, exit! false, abort 1.

for a senior

Show where this matters: cleanup placed in ensure, code that rescues SystemExit and cancels an exit, forked children using exit!, and libraries that must raise instead of exiting.

for a principal

Set the convention for a team's scripts: one entry point, statuses documented, libraries never exit, and exit! reserved for reviewed cases such as forked workers.

## Three ways to stop, two mechanisms Ruby offers three `Kernel` methods to end a program, and they fall into two mechanisms: raising an exception that unwinds normally, or terminating the process immediately. | Method | Default status | Mechanism | `ensure` runs | `at_exit` runs | Can be rescued | |---|---|---|---|---|---| | `exit(status = true)` | 0 | raises `SystemExit` | yes | yes | yes | | `abort(msg = nil)` | 1 | prints `msg` to `$stderr`, raises `SystemExit` | yes | yes | yes | | `exit!(status = false)` | 1 | immediate process exit | no | no | no | The same trio exists as `Process.exit`, `Process.abort` and `Process.exit!`. ## `exit` is an exception `exit` raises **`SystemExit`**, which carries the status (`SystemExit#status`) and answers `success?`. Because the exception propagates like any other: - every `ensure` clause between the call and the top level runs, which is where lock files are removed and temporary files deleted; - once the exception reaches the top level, Ruby runs the `at_exit` handlers (in reverse order of registration) and object finalizers, flushes its IO buffers and returns the status; - the status can be `true` (0), `false` (1) or an integer. `SystemExit` inherits directly from `Exception`, outside `StandardError`, so ordinary error handling lets it pass. Code that rescues it explicitly (`rescue SystemExit => e`) can read `e.status` and either re-raise or carry on, in which case the exit is cancelled and the program keeps running. ## `abort` is `exit(false)` with a message `abort("rotation failed: disk full")` writes the message and a newline to standard error, then raises `SystemExit` with status 1, and the message becomes the exception's message. Called with no argument while an exception is being handled, `abort` prints that exception's message and backtrace before exiting. It is the natural way for a script to fail with a human-readable reason. ## `exit!` skips everything `exit!` calls the operating system's immediate exit. Nothing on the Ruby side runs afterwards: 1. no `ensure` clause, so cleanup written there is skipped; 2. no `at_exit` handler or `END` block; 3. no finalizer, and output still sitting in Ruby's IO buffers can be lost. Its default status is `false` (1), the opposite of `exit`. The classic legitimate use is ending a forked child process that must not run the parent's exit handlers a second time. ## Seeing the difference ```ruby at_exit { puts "at_exit: remove lock" } begin puts "rotating" exit 2 ensure puts "ensure: close files" end # prints: rotating / ensure: close files / at_exit: remove lock # status: 2 ``` Replace `exit 2` with `exit! 2` and the `ensure` and `at_exit` lines never appear; the status is still 2. ## Choosing in practice - A command-line script that failed: `abort "reason"` (message plus status 1) or `exit 1` after logging. - A script that succeeded early: `exit` or `exit true`. - A library: never call `exit`; raise a specific exception and let the caller decide. - `exit!`: only when you deliberately must bypass handlers, and you accept losing unflushed output. Falling off the end of the main script returns 0, and an uncaught exception other than `SystemExit` prints its message and backtrace and returns 1. ## Misconceptions interviewers listen for - "`exit` kills the process": it raises an exception, so `ensure` runs and the exit can be intercepted. - "`exit!` is just a louder `exit`": it is a different mechanism, with a different default status (1) and no cleanup at all. - "`abort` skips cleanup": it raises the same kind of `SystemExit` as `exit(false)`, after printing its message. - "The status must be an integer": `true` and `false` are accepted and mean 0 and 1. ## Reading the status back A `SystemExit` object answers `status` (the integer) and `success?` (whether it is 0). Inside an `at_exit` handler, `$!` holds that object, which is how a handler knows whether the program is ending in failure. From the shell, `echo $?` after the run shows the number the parent received.

  • What is `$!` inside an `at_exit` handler after `abort "disk full"`?
    A `SystemExit` whose `status` is 1 and whose `message` is `"disk full"`, because `abort` raises `SystemExit` after writing the message. Handlers can inspect `$!.success?` to decide whether to report a failure.
  • Why is `exit!` the usual way to end a forked child process?
    The child inherits the parent's `at_exit` handlers. Ending it with `exit` would run them again in the child, closing shared connections or printing a second report. `exit!` terminates the child without running them.
  • What status does a Ruby script return when an uncaught `RuntimeError` ends it?
    Status 1. Ruby runs the `at_exit` handlers, then prints the error message and backtrace to standard error, and exits with failure. Only `SystemExit` carries its own status out of the program.

saying these in an interview costs you the question

  • exit ends the process immediately, so ensure blocks are skipped
  • exit! runs at_exit handlers but skips ensure clauses
  • Once exit is called, no Ruby code can stop the process ending
  • exit! defaults to status 0, the same as exit
  • abort exits without running ensure or at_exit handlers