skip to content

In Ruby, how is Exception#cause set when you raise a new error inside a rescue clause, and how do you control or suppress it?

level: seniorimportance: should knowfreq 42%

answer

  1. automatic from $!
  2. cause: keyword overrides it
  3. cause: nil suppresses it
  4. printed by full_message
  5. circular causes: ArgumentError

basics

~20 s

When raise runs while another exception is being handled, Ruby stores that exception ($!) as the new error's cause automatically. Pass cause: other_error to choose it, or cause: nil to drop it; full_message and the uncaught-error printer show the chain.

solid answer

~40 s

`Kernel#raise` takes a `cause:` keyword whose default is `$!`, the exception currently being handled. So `rescue Net::ReadTimeout => e` followed by `raise ShippingRates::CarrierTimeout, "UPS did not answer"` produces an error whose `cause` is the timeout, with its own backtrace, without any extra code. You can pass `cause: some_error` to attach a different exception, or `cause: nil` to deliberately break the chain, for example when the low-level error would leak credentials. Ruby refuses `cause:` without an exception argument and rejects chains that would loop (`circular causes`). The chain is printed by `Exception#full_message` and by Ruby's handler for uncaught exceptions, and error reporters can walk it with `e.cause` until `nil`.

code

ruby · 18 lines
ruby
require "net/http"

module ShippingRates
  class Error < StandardError; end
  class CarrierUnavailable < Error; end

  def self.quote(http)
    http.get("/rates").body
  rescue Net::OpenTimeout, Net::ReadTimeout, SocketError
    raise CarrierUnavailable, "carrier did not answer"   # cause: the network error
  end
end

begin
  ShippingRates.quote(Net::HTTP.new("rates.example"))
rescue ShippingRates::Error => e
  warn "#{e.class}: #{e.message} (cause: #{e.cause.class})"
end

go deeper

for a junior

Know that e.cause returns the exception that was being handled when e was raised.

for a middle

Explain the cause: $! default, the cause: override and cause: nil, and where the chain is printed.

for a senior

Wrap low-level errors inside the rescue clause so causes are kept, walk chains in reporters, and cut or scrub chains that would leak secrets.

for a principal

Decide how errors cross library and service boundaries: which layers wrap, what the cause chain may carry, and what reporters must redact.

## What cause is Every exception object has a **`cause`** method returning another exception or `nil`. It records **which error was being handled when this one was raised**. Together, `cause` links form a **cause chain**: the high-level error you rescue at the top, then the lower-level error it was raised in response to, and so on. ## How Ruby sets it `Kernel#raise` is documented as `raise(exception, message = exception.to_s, backtrace = nil, cause: $!)`. The default of the keyword is the global **`$!`**, which holds the exception being handled inside a rescue clause and is `nil` elsewhere. Therefore: 1. Outside any rescue, a new error's `cause` is `nil`. 2. Inside a rescue clause, raising a new error sets `cause` to the rescued exception automatically. 3. The same applies inside `ensure` while an exception is propagating: an error raised there gets the in-flight exception as its cause. 4. Ruby avoids a self-reference: re-raising the same object (`raise` or `raise e`) does not make it its own cause. ## Controlling it with the keyword | Call | Resulting `cause` | |---|---| | `raise CarrierTimeout, "UPS did not answer"` inside `rescue` | the rescued error | | `raise CarrierTimeout, "...", cause: original` | `original` | | `raise CarrierTimeout, "...", cause: nil` | `nil`, chain deliberately cut | | `raise cause: original` | `ArgumentError`: only cause is given with no arguments | The `cause:` value must be an exception or `nil`; anything else raises `TypeError`. If the new exception already appears in the given cause's chain, Ruby raises `ArgumentError` with `circular causes`, so the chain always terminates. ## Where the chain shows up - **`Exception#full_message`** prints the error, its backtrace, and then each cause with its own message and backtrace. - **Ruby's uncaught-exception output** uses the same printer, so a crash shows the whole chain. - **Your code** can walk it: `while e; log(e.class, e.message); e = e.cause; end`. - Error trackers commonly read `cause` to show the original failure under the wrapper. ## Why it matters when you wrap errors A shipping-rates gem that turns `Net::ReadTimeout` into `ShippingRates::CarrierTimeout` gives callers one stable class to rescue. Without `cause`, the network detail would be lost; with it, the high-level error is what callers handle and the low-level one is still there for debugging. Because Ruby sets it automatically, the common mistakes are the reverse ones: - **Raising the wrapper outside the rescue clause**, for example after the `begin` block ends, so `$!` is already `nil` and no cause is recorded. - **Copying the message instead of relying on cause**, `raise Wrapper, e.message`, which duplicates text and still loses the original backtrace if the cause was cut. - **Cutting the chain by accident** with `cause: nil` copied from elsewhere. ## Security note The cause travels with the exception. If the low-level error's message contains a URL with a token or a database connection string, printing `full_message` or sending the error to a tracker exposes it. Either scrub the message when wrapping or pass `cause: nil` for that specific case and log a redacted summary instead. ## Reading a chain in practice When a wrapped error reaches the top of a process, the printed report shows the wrapper first and then the cause, each with its own backtrace. Read it bottom-up to find the root failure: the last cause is usually the network, file or parsing error that started it, and the wrappers above it show which layers translated it. In code, a small loop over `e.cause` collects the classes for a single log line, and error trackers that understand `cause` display the same structure as nested entries.

  • Does raise e inside the rescue clause make e its own cause?
    No. Re-raising the object being handled, with a bare `raise` or `raise e`, raises the same exception, and Ruby skips a cause that would equal the exception itself. The cause stays whatever it was when the error was first raised.
  • Why might you pass cause: nil on purpose?
    When the underlying error must not travel further, for example because its message holds a signed URL or a connection string, or when it is irrelevant noise. `cause: nil` cuts the chain so `full_message`, crash output and error trackers do not show it. Log a redacted summary separately if it is still useful.
  • What happens with raise cause: err and no class or message?
    Ruby raises `ArgumentError` with `only cause is given with no arguments`. The keyword cannot turn a bare `raise` into a re-raise with a different cause; name the exception to raise as well.

Like forwarding an email with the original quoted underneath: the recipient acts on your new message, but the original, headers and all, stays attached unless you deliberately delete it before sending.

saying these in an interview costs you the question

  • You must always pass cause: explicitly or the original is lost
  • A bare raise inside rescue sets the exception as its own cause
  • cause: accepts a message string as well as an exception
  • full_message prints only the outermost exception
  • Raising the wrapper after the begin block still records the cause