In Ruby, what happens when an exception escapes a thread's block, and what do report_on_exception and abort_on_exception change?
answer
- the thread dies, others continue
- report_on_exception true since 2.5
- report goes to $stderr at death
- join or value re-raises later
- abort_on_exception false; -d turns it on
basics
~20 sThe thread dies while others keep running. report_on_exception (default true) prints the error to $stderr at that moment; join or value re-raise it later. abort_on_exception (default false) re-raises it in the main thread immediately instead.
solid answer
~40 sAn exception that escapes a `Thread` block terminates only that thread. By default Ruby does three things: it prints `#<Thread:...> terminated with exception (report_on_exception is true):` and the backtrace to `$stderr`, it stores the exception with the thread, and it re-raises it in any thread that later calls `join` or `value`. `report_on_exception` controls only the printed report; it defaults to `true` since Ruby 2.5 and a thread copies the global value when it is created. `abort_on_exception` defaults to `false`; set globally (`Thread.abort_on_exception = true`), per thread, or via `$DEBUG`/`-d`, it re-raises the exception in the **main** thread as soon as the worker dies, which usually ends the program. Turn reporting off only for threads you are certain to join.
code
ruby · 11 linesworker = Thread.new { raise KeyError, "rate missing" }
sleep 0.1
# $stderr: #<Thread:0x... run> terminated with exception (report_on_exception is true):
# ... KeyError: rate missing
worker.status # => nil (died from an exception)
quiet = Thread.new do
Thread.current.report_on_exception = false
raise KeyError, "rate missing"
end
quiet.join # re-raises KeyError here; nothing was printed earliergo deeper
Recall that an unrescued exception kills only its thread, prints a report by default, and is re-raised by join or value.
Explain the two flags: report_on_exception only controls the stderr report and is copied at creation; abort_on_exception re-raises in the main thread.
Show how you keep workers alive with per-job rescue and logging, and when a fail-fast abort_on_exception is the better choice for scripts and tests.
Set a team convention for background thread failures: what is logged, what restarts, and how a silently dead worker is detected in production.
## What happens by default When code inside `Thread.new { ... }` raises and nothing in the block rescues it, CRuby handles it in this order: 1. The thread terminates. Other threads, including the main thread, keep running. 2. Because `report_on_exception` is `true` by default, Ruby writes a report to `$stderr`: the thread's `inspect` string, `terminated with exception (report_on_exception is true):`, and the backtrace. 3. The exception is kept with the thread. `thr.status` now returns `nil` (a normal finish returns `false`). 4. Any later `thr.join` or `thr.value` re-raises the same exception in the calling thread. The flag arrived in Ruby 2.4 defaulting to `false`, and before Ruby 2.5 step 2 therefore did not happen by default: a worker that crashed and was never joined vanished without a trace. Ruby 2.5 flipped the default to `true` because that silence hid bugs. ## report_on_exception - **Global**: `Thread.report_on_exception` / `Thread.report_on_exception = false`. Default `true`. - **Per thread**: `thr.report_on_exception = false`, often written as the first line of the block with `Thread.current`. - **When it is read**: a new thread **copies** the global flag at creation. Changing the global value later does not affect threads already running. - **What it changes**: only the `$stderr` report. The thread still dies, and `join`/`value` still re-raise. The core documentation's advice is to disable it only for threads guaranteed to be joined, because otherwise a failure may be handled much later or never. ## abort_on_exception - **Global**: `Thread.abort_on_exception = true`. Default `false`. - **Per thread**: `thr.abort_on_exception = true`. - **Also enabled by**: `$DEBUG`, which the `-d` command-line flag sets. - **What it changes**: when any affected thread dies from an exception, Ruby re-raises that exception **in the main thread** right away. Unless the main thread rescues it at that point, the process ends. The global flag is checked when the thread dies, so turning it on also covers threads started earlier. ## Side by side | Setting | Default | Printed report | Main thread interrupted | `join`/`value` re-raise | |---|---|---|---|---| | defaults | report on, abort off | yes | no | yes | | `report_on_exception = false` | per thread or global | no | no | yes | | `abort_on_exception = true` | per thread or global | yes, if reporting is on | yes, immediately | yes | ## Choosing settings in practice - **Fan-out that you join** (fetching 50 exchange-rate pages and calling `value` on each): keep the defaults, or turn the report off inside the block if the `value` caller logs the error itself. - **Long-lived background workers** that nobody joins: rescue inside the loop, log with context, and decide whether to retry. The default report is a last-resort signal, not error handling. - **Scripts and tests** where any worker failure should stop everything: `Thread.abort_on_exception = true` near the top makes failures loud and immediate. A common production bug is a worker thread that dies on the first unexpected error while the process keeps serving, so the queue it drained silently stops moving. A `begin`/`rescue => e` around each unit of work, plus a log line, keeps the worker alive and the failure visible. ## Edge cases - `SystemExit` raised in a non-main thread (for example by calling `exit` there) is re-raised in the main thread regardless of these flags, which normally ends the program. - `Thread#kill` ends a thread without an exception reaching `join`; `status` is then `false`. - A rescued exception inside the block never reaches either mechanism. - The report is written with the thread's `inspect` string, so giving workers a name with `Thread#name=` (for example `"rates-fetch-3"`) makes the `$stderr` line identify which worker died. ## What an interviewer is checking The question separates three ideas candidates often merge: 1. **Termination**: an unrescued exception always ends its own thread, whatever the flags say. 2. **Visibility**: `report_on_exception` decides whether you hear about it at the moment it happens. 3. **Propagation**: `join`/`value` always re-raise it later, and `abort_on_exception` additionally re-raises it in the main thread at once. A candidate who can say which flag controls which of these, and which default applies, understands the mechanism; one who says "exceptions in threads are swallowed" is describing Ruby before 2.5, or code that never joins its threads.
- Why does setting Thread.report_on_exception = false late in a program not silence workers already running?Each thread copies the global flag when it is created. Threads started before the change keep `true`. To silence one of them, set `thr.report_on_exception = false` on that thread, typically as the first line of its block.
- How do you keep a long-lived worker thread alive when one job raises?Rescue inside the loop around each unit of work, not around the whole loop: `loop { job = next_job; begin; process(job); rescue => e; log(e, job); end }`. The thread survives, the failure is logged with context, and the report and abort flags never fire.
saying these in an interview costs you the question
- An exception in any thread crashes the whole Ruby process by default.
- Thread exceptions are silent unless you call join.
- report_on_exception = false makes join stop re-raising the exception.
- abort_on_exception is true by default in modern Ruby.
- Setting Thread.report_on_exception later affects threads already running.