A Ruby worker loop wraps each job in rescue Exception => e and logs it; why can't Ctrl-C or a TERM signal stop it?
answer
- signals become exceptions
- Ctrl-C raises Interrupt
- TERM raises SignalException
- exit raises SystemExit too
- Lint/RescueException flags it
basics
~20 sBy default Ruby turns Ctrl-C into an Interrupt and TERM into a SignalException raised in the main thread, and exit raises SystemExit. All inherit from Exception, so rescue Exception catches them, logs them and the loop continues. Rescue StandardError instead.
solid answer
~40 sBy default Ruby handles SIGINT by raising `Interrupt` in the main thread and SIGTERM (also HUP, QUIT and others) by raising `SignalException`; `exit` raises `SystemExit`. None of them is a `StandardError`, but all are `Exception`s, so a `rescue Exception => e` inside the loop catches the shutdown request, logs it as if it were a failed job and moves on to the next job. The fix is to rescue `StandardError` (or a bare `rescue`), which still catches every ordinary job failure while letting signals and `exit` propagate and end the loop. If a catch-all for reporting is really needed, it must re-raise signals and `SystemExit` instead of swallowing them. RuboCop's `Lint/RescueException`, enabled by default, flags the pattern.
code
ruby · 7 lines# Broken: Ctrl-C raises Interrupt, which is caught here; the loop goes on.
loop do
job = fetch_next_job # blocks until work arrives
job.call
rescue Exception => e
warn "job failed: #{e.class}: #{e.message}"
endgo deeper
Remember that Ctrl-C and exit work by raising exceptions that are not StandardErrors, and that rescue Exception catches them.
Walk through the chain: SIGINT raises Interrupt, SIGTERM raises SignalException, exit raises SystemExit, all under Exception; then show the StandardError fix.
Tie it to operations: deploys that time out and escalate to kill -9, skipped ensure cleanup, and a graceful-shutdown flag instead of catching signals in job code.
Decide where catch-all handling is allowed: one audited top-level reporter that re-raises, enforced by Lint/RescueException everywhere else.
## The symptom A background worker pulls jobs from a queue forever. To keep one bad job from killing the worker, someone wrapped the body in `rescue Exception => e`. Now pressing Ctrl-C prints a log line and the worker keeps going; a deploy tool sending TERM sees the same thing, waits for its timeout and eventually has to kill the process forcefully. Only `kill -9` works. ## How signals reach Ruby code CRuby installs its own handlers for several signals at startup. When no custom handler has been registered, it converts the signal into an **exception raised in the main thread**: | Signal | Exception raised | Class chain | |---|---|---| | INT (Ctrl-C) | `Interrupt` | `Interrupt < SignalException < Exception` | | TERM, HUP, QUIT, ALRM, USR1, USR2 | `SignalException` (e.g. message `SIGTERM`) | `SignalException < Exception` | | KILL | nothing; the process is killed by the kernel | cannot be caught | A related exception comes from inside the program: `Kernel#exit` works by raising **`SystemExit`**, which also inherits directly from `Exception`. The design is deliberate. Turning a signal into an exception lets `ensure` blocks run during shutdown, so files get closed and locks released. It also means shutdown travels through the same `rescue` machinery as errors. ## Why rescue Exception breaks it A rescue clause matches the listed class and all its subclasses. `Exception` is the root, so `rescue Exception` matches everything, including: - the `Interrupt` from Ctrl-C; - the `SignalException` from TERM; - the `SystemExit` from an `exit` call inside a job; - `NoMemoryError`, `SystemStackError` and `ScriptError`s such as `LoadError`. Inside a `loop do ... end` block with a block-level rescue, catching the exception ends only the current iteration, and the loop starts the next one. The request to stop has been converted into a log line. ## The fix 1. **Rescue `StandardError`**, either by name or with a bare `rescue`. Every ordinary job failure (`ArgumentError`, `KeyError`, `Errno::*`, `RuntimeError`, custom errors that inherit from `StandardError`) is still caught, logged and skipped. 2. **Let signals and `SystemExit` propagate.** With the narrower rescue, `Interrupt` leaves the loop, unwinds through `ensure` blocks and ends the program the normal way. 3. **If you need a true catch-all** at the very top of the process, for example to report crashes, rescue `Exception` there only, and re-raise so termination still happens. 4. **For graceful shutdown** (finish the current job, then stop), register a TERM handler that sets a flag the loop checks, rather than relying on catching the signal exception. ## Tooling that catches it RuboCop's `Lint/RescueException` cop is enabled by default and reports any rescue clause that names `Exception`, with the message suggesting `StandardError` instead. It fires even when the body re-raises, so a deliberate top-level handler needs an inline disable comment explaining why. `Style/RescueStandardError` is a separate cop: it only decides whether you write `rescue` or `rescue StandardError`, and both spellings have the safe scope. ## Worth remembering - The loop is not immune to `kill -9`; SIGKILL cannot be handled by any process, so it is the only signal the broken worker still obeys. - Signals are delivered to the main thread. A worker loop in a secondary thread behaves differently, which is a threading question rather than a hierarchy one. - The same mistake hides `exit` calls: a job that calls `exit` to abort the worker simply logs `SystemExit` and the loop carries on. ## Diagnosing a worker that will not stop When a process ignores Ctrl-C or TERM, work through the likely causes in order: 1. **Search for broad rescues.** `rescue Exception` inside a loop, a job wrapper or a retry helper is the usual culprit; running RuboCop with `Lint/RescueException` enabled lists them. 2. **Check the logs for signal classes.** A log line showing `Interrupt` or `SignalException: SIGTERM` as a "failed job" confirms the signal was caught and swallowed. 3. **Look for rescued `SystemExit`.** A line reporting `SystemExit` means `exit` was called and ignored. 4. **Confirm which thread runs the loop.** Signals are raised in the main thread; a loop running in a secondary thread never sees them, so its shutdown needs a different mechanism. Once the broad rescue is narrowed to `StandardError`, the same signals end the loop immediately.
- Why does kill -9 still stop the worker that rescues Exception?SIGKILL cannot be caught, blocked or handled by any process, so the kernel ends the process without Ruby ever raising anything. Signals such as INT and TERM are different: Ruby has handlers for them and turns them into `Interrupt` and `SignalException`, which the broad rescue swallows.
- What happens when a job inside that loop calls exit?`exit` raises `SystemExit`, a direct subclass of `Exception`. The `rescue Exception` clause catches it, logs it and the loop continues, so the process does not exit. With `rescue StandardError` the `SystemExit` propagates and the program ends after running `ensure` blocks.
- Is rescue Exception ever acceptable?At the outermost edge of a process, a handler may rescue `Exception` to report a crash, provided it re-raises so signals and `exit` still terminate. Inside loops, jobs or request handlers it is a defect, and RuboCop's `Lint/RescueException` reports it either way, so the deliberate case carries a disable comment.
Like a receptionist told to take a message for every call who also takes a message when the evacuation alarm rings, then stays at the desk. The instruction was meant for ordinary calls; narrowing it to ordinary calls (StandardError) lets the alarm do its job.
saying these in an interview costs you the question
- Ctrl-C raises a StandardError, so rescue StandardError would trap it too
- rescue Exception is the safest choice because it catches everything
- SIGTERM kills a Ruby process without raising anything Ruby code can see
- Calling exit inside a job always ends the process
- Logging the exception makes rescue Exception harmless