In Ruby, why do LoadError and NotImplementedError escape a bare rescue, and how should code rescue them deliberately?
answer
- a third branch under Exception
- ScriptError: load, syntax, not implemented
- rescue LoadError around one require
- modifier rescue misses it too
- fork unsupported: respond_to? false
basics
~10 sLoadError, SyntaxError and NotImplementedError inherit from ScriptError, which sits directly under Exception, not StandardError. A bare rescue or rescue modifier skips them; name the class, as in rescue LoadError around one optional require.
solid answer
~40 s`ScriptError` is a sibling of `StandardError` under `Exception`, and its subclasses are `LoadError` (a `require` or `load` that cannot find or load a file), `SyntaxError` (code that does not parse, for example through `eval` or `load`) and `NotImplementedError`. Ruby's own documentation says these are not `StandardError` and are rescued only when named. So `rescue => e` and `require "oj" rescue nil` both let a missing gem crash the program. For an optional dependency, wrap just that `require` in `begin ... rescue LoadError ... end`. `NotImplementedError` is documented for platform features that are unavailable, like `fork` where the OS lacks it; code that uses it as an abstract-method marker should know callers' bare rescues will not catch it.
code
ruby · 7 linesbegin
require "oj"
JSON_ENGINE = :oj
rescue LoadError
require "json"
JSON_ENGINE = :json
endgo deeper
Remember that LoadError, SyntaxError and NotImplementedError are ScriptErrors, and that a bare rescue does not catch them.
Explain the optional-dependency idiom: a small begin around one require, rescue LoadError by name, and why the rescue modifier fails here.
Show judgment about scope: keep the begin block tiny so a broken internal file is not mistaken for a missing optional gem, and log which path ran.
Weigh NotImplementedError as an abstract-method marker against a StandardError subclass, given that callers' generic handlers will not catch it.
## A third branch of the tree Ruby's exception tree has **`Exception`** at the root. Most application errors live under **`StandardError`**, which is the class a bare `rescue` catches. Next to it, directly under `Exception`, is **`ScriptError`**, described in its class documentation as the superclass for errors raised when a script cannot be executed. It has three built-in subclasses: | Class | Raised when | Parent | |---|---|---| | `LoadError` | `require` or `load` cannot find or load a file, e.g. a gem that is not installed | `ScriptError` | | `SyntaxError` | code does not parse, e.g. through `eval` or `load` of a broken file | `ScriptError` | | `NotImplementedError` | a feature is unavailable on this platform, e.g. `fork` where the OS lacks it | `ScriptError` | The documentation adds the rule that matters: these are **not** `StandardError` and **will not be rescued unless specified explicitly**, or through their ancestor `Exception`. ## Why they bypass generic handlers All three mean the program itself is incomplete: a file is missing, source does not parse, or a capability does not exist here. A generic handler written for bad input or a flaky network would hide such a defect and keep running with half the code loaded. Keeping them outside `StandardError` makes them loud by default. This catches people in three common places: - **`rescue => e`** around code that lazily loads a library: the `LoadError` sails past. - **The rescue modifier**, `require "oj" rescue nil`: the modifier has the same `StandardError` scope, so a missing gem still raises. The `StandardError` class documentation uses exactly this case, `require 'does/not/exist' rescue "Hi"`, as its example of what is *not* rescued. - **An abstract method** written as `raise NotImplementedError`: a caller's bare rescue does not catch it, and the error escapes to the top level. ## Rescuing LoadError deliberately The idiom for an optional dependency is narrow on both axes, one class and one statement: 1. Put only the `require` inside the `begin`. 2. Rescue `LoadError` by name. 3. In the rescue body, choose the fallback and record which path was taken. Keeping the `begin` block small matters because `LoadError` from any nested `require` would otherwise be treated as "the optional gem is missing", even when it is a broken internal file. Under Bundler, remember that a gem missing from the Gemfile also shows up as `LoadError`, so the fallback path can hide a Gemfile mistake; log it. ## NotImplementedError: what it is for Ruby raises `NotImplementedError` from core methods that the current platform cannot support. The documentation gives `fork` and `fsync` as examples and adds a useful detail: when `fork` raises `NotImplementedError`, `respond_to?(:fork)` returns `false`, so feature detection with `respond_to?` works. Many codebases also write `raise NotImplementedError` in a base-class method that subclasses must override. That idiom is common enough that RuboCop's `Lint/UnusedMethodArgument` has an `IgnoreNotImplementedMethods` option that recognises it. If you use it, remember the consequence: the error is a `ScriptError`, so `rescue => e` in calling code will not handle it, which is often what you want for a programming mistake but surprises people who expect it to behave like `NoMethodError`. ## SyntaxError at runtime `SyntaxError` is not only a boot-time failure. Code that calls `eval`, or `load`s plugin files at runtime, can raise it later. A plugin loader that must survive a broken plugin rescues `SyntaxError` and `LoadError` by name, around the single `load` call, and reports which file failed. Rescuing `ScriptError` wholesale around a whole application would hide real deploy defects and is rarely right. ## Checking the tree yourself You do not need to memorise every branch. Ruby answers the question directly: - `LoadError.superclass` returns `ScriptError`, and `ScriptError.superclass` returns `Exception`. - `LoadError <= StandardError` returns `nil`, because the two classes are unrelated; `LoadError <= ScriptError` returns `true`. - `LoadError#path` returns the path that failed to load, which is useful in the log line of an optional-dependency fallback. In short, the ScriptError branch is Ruby saying "the program is not whole", and it is kept out of generic handlers so that condition stays visible.
- Does require "oj" rescue nil handle a missing gem?No. The rescue modifier catches only `StandardError`, and a failed `require` raises `LoadError`, a `ScriptError`. The statement raises as if the modifier were absent. Use `begin; require "oj"; rescue LoadError; ... end` around that single require.
- When does Ruby itself raise NotImplementedError?For core methods the platform cannot support, such as `fork` or `fsync` on systems without them. Ruby also arranges that `respond_to?(:fork)` returns `false` in that case, so feature detection with `respond_to?` avoids the exception altogether.
- Why not rescue ScriptError around the whole application?It would also swallow `SyntaxError` from code you meant to run and `LoadError` from a broken internal file, so the program keeps going with parts missing. Rescue the specific class around the specific `require` or `load` whose failure you have a plan for.
saying these in an interview costs you the question
- LoadError is a StandardError, so a bare rescue handles a missing gem
- NotImplementedError inherits from StandardError like other errors
- require "x" rescue nil is a safe optional-dependency guard
- Wrap the whole app in rescue ScriptError to be safe
- SyntaxError can only happen when the program first boots