Ruby exception handling interview questions, with answers
Ruby reports errors with exceptions, and its syntax for handling them is compact enough that it is easy to rescue too much or too little. Interviews check whether a candidate knows the hierarchy, which exceptions a bare rescue catches, and how to guarantee cleanup when something fails.
The answers below cover begin, rescue, else and ensure, the exception hierarchy, custom exceptions, raise, cleanup in methods that take blocks, the rescue modifier and exception causes, with code run on Ruby 3.4. Then take the free Ruby diagnostic — ten questions across every Ruby topic in the bank — to see which of these you can explain but not yet predict.
The questions, with answers
1.How does exception handling work in Ruby?
In short: Code runs in begin; rescue clauses handle matching exceptions, else runs if none was raised, and ensure always runs last.
A begin block runs its code, and if an exception is raised, Ruby looks for the first rescue clause whose class matches, binding the exception with => e. An else clause runs only when the begin block raised nothing, and ensure runs in every case, which makes it the place for cleanup. A method body, and since Ruby 2.6 a do...end block, can use rescue, else and ensure directly without writing begin. An exception with no matching rescue propagates to the caller and, at the top level, ends the program with a message and backtrace.
begin 1 / 0 rescue ZeroDivisionError => e puts "bad: #{e.class}" else puts "fine" ensure puts "done" end # bad: ZeroDivisionError # done
2.Why should Ruby code rescue StandardError rather than Exception?
In short: A bare rescue catches StandardError and its subclasses; rescuing Exception also traps signals and exits, such as Ctrl-C and exit.
Exception is the root of the hierarchy. Most errors a program should handle, such as ArgumentError, NoMethodError, KeyError and ZeroDivisionError, descend from StandardError, and a rescue with no class, or rescue => e, catches exactly those. Exception's other branches include Interrupt, raised by Ctrl-C, SystemExit, raised by exit, and NoMemoryError, which should normally end the program. Rescuing Exception swallows them and can make a process impossible to stop. Custom exceptions should therefore inherit from StandardError.
p KeyError.superclass p IndexError.superclass p StandardError.superclass # IndexError # StandardError # Exception
3.How do you define a custom exception in Ruby?
In short: Subclass StandardError, optionally overriding initialize to build the message, and rescue it by class.
class Overdue < StandardError; end is a complete custom exception. Callers can rescue Overdue specifically and let everything else pass. Overriding initialize and calling super with a message builds a consistent message from data, and extra attributes can carry details for the handler. raise Overdue, "book" creates the exception by calling Overdue.new("book"). Libraries usually define one base error class, such as MyGem::Error, and subclass it, so users can rescue all of the library's errors with one clause.
class Overdue < StandardError def initialize(item) super("overdue: #{item}") end end begin raise Overdue, "book" rescue Overdue => e puts e.message end # overdue: book
4.What does raise do in Ruby?
In short: raise "msg" raises a RuntimeError, raise Cls, "msg" raises that class, and a bare raise inside rescue re-raises the current exception.
raise has several forms. With only a string, it raises RuntimeError with that message. With a class and a message, it creates and raises an instance of that class, and it also accepts an exception object that already exists. Inside a rescue clause, raise with no arguments re-raises the exception being handled, which lets code log a failure and pass it on. fail is an alias. Every exception carries message, full_message and backtrace, the list of calls that led to it.
begin raise "oops" rescue => e p e.class, e.message end # RuntimeError # "oops"
5.How do Ruby methods that take blocks guarantee cleanup?
In short: The method yields inside a body with ensure, so its cleanup runs even when the block raises, before the exception reaches the caller.
This is the pattern behind File.open with a block, Mutex#synchronize and database transactions: the method acquires a resource, yields to the caller's block, and releases the resource in an ensure clause. If the block raises, ensure still runs, and then the exception continues to the caller, as the order of the output shows. Callers therefore never forget to close or unlock anything, which is why the block forms are preferred over calling open and close separately.
def locked puts "lock" yield ensure puts "unlock" end begin locked { raise "boom" } rescue => e puts e.message end # lock # unlock # boom
6.What is the rescue modifier in Ruby, and when is it risky?
In short: expr rescue fallback returns fallback if expr raises a StandardError; it is concise but hides every such error, not just the expected one.
Like if and unless, rescue can follow an expression: value = risky rescue default. If the expression raises any StandardError, the result is the fallback. It is compact for genuinely optional work, but it cannot name an exception class, so a typo that raises NoMethodError is silently replaced by the fallback too. Prefer a method that avoids the exception, such as fetch with a default or Integer(str, exception: false), or a full rescue clause naming the class you expect.
v = [].fetch(1) rescue -1 p v # -1
7.What is the cause of a Ruby exception?
In short: When an exception is raised inside a rescue clause, Ruby stores the exception being handled in the new one's cause.
Code often translates a low-level error into a higher-level one, such as a ZeroDivisionError into an application error. Ruby links them automatically: an exception raised while another is being rescued records the original in its cause, so e.cause below returns the ZeroDivisionError. Error reporters and full_message show the whole chain, which keeps the root cause visible. raise also accepts cause: explicitly, including cause: nil to drop the link on purpose.
begin begin 1 / 0 rescue raise "load failed" end rescue => e p e.message, e.cause.class end # "load failed" # ZeroDivisionError
How the diagnostic asks it
One question from the Ruby bank, exactly as a sitting would show it. The bank has 4 on exceptions and 30 across Ruby.
What does this Ruby code print?
puts "12x".to_i begin Integer("12x") rescue => e puts e.class end
- 10, then ArgumentError
- 212, then 12
- 312, then ArgumentErrorcorrect
- 4It raises TypeError before printing anything
String#to_i is lenient: it parses as many leading digits as it can and ignores the rest, returning 12 for "12x" and 0 for a string with no leading digits, so it never raises. Kernel#Integer is strict: the whole string must be a valid integer, so Integer("12x") raises ArgumentError, invalid value for Integer(). rescue with no class catches StandardError, which includes ArgumentError, and prints its class. 0 assumes to_i rejects the whole string. 12 twice assumes Integer() is as lenient as to_i. TypeError is raised for arguments such as nil, not for a malformed string. Use Integer() to validate input.
Measure it
Reading answers tells you what’s true. A diagnostic tells you what you get wrong.
10 Ruby questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.