LaunchKit

Ruby on Rails interview questions, by what they test

September 11, 2026

Ruby on Rails interview questions, by what they test

A Ruby on Rails interview of the technical kind rarely asks for a definition. It puts five lines of Ruby on the table and asks what they return, because reading a short program is the cheapest way to find out whether a candidate has read the language or only used it.

Twenty-three questions of that shape are grouped below by the behaviour each one probes. Most of them are Ruby interview questions rather than framework trivia: twenty-one are about the language itself and two are about Rails, which is roughly where the difficulty lives. Every group states what the code actually does, names the Ruby or Rails documentation that settles it, and links the page holding the runnable answer.

What changes between a five-year answer and a ten-year one

A year count is not a property of a question. Hash.new([]) behaves the same way in front of a candidate with two years and one with twelve. What changes is the layer the answer reaches for.

The first layer names the output: "it prints an empty hash". The second names the mechanism: the default object is shared, and << mutates that one object without ever storing it under the key. The third names the cost: a counter built this way aggregates across keys, and the bug surfaces in production as one record carrying another record's totals.

Three cards side by side under a shallow-to-deep arrow. Layer one, the output: "it prints an empty hash". Layer two, the mechanism: the default object is shared and never stored under the key. Layer three, the production bug: one record carries another record's totals.

Most candidates reach the first layer. Reaching the second is what an interviewer is usually listening for after a few years. Reaching the third, unprompted, is what a senior answer sounds like, and it is the one thing that can be practised deliberately: for each behaviour below, ask what it costs in production rather than what it prints in IRB.

How to read the groups below

The twenty-three questions fall into eight subjects. Each group opens with the behaviour under test, settles it against the Ruby or Rails documentation, and links the quiz page that holds the runnable version with its choices and its explanation.

Use the summaries to find the behaviours you are shaky on, then go to the quiz page and answer it before reading the explanation. Recognising an answer and producing one out loud are different skills, and only the second is the one being tested across a table. The groups are independent, so there is no order to follow: start with the subject you would least like to be asked about.

Bang methods and mutation in place

Three questions ask one thing in three shapes: does this call change the object, return a copy, or do neither.

The Ruby core documentation for String settles two of them in a line each. chomp! "Removes the trailing record separator, if found; returns self if any changes, nil otherwise", and upcase! "Upcases all characters; returns self if any changes, nil otherwise". A bang method that finds nothing to change returns nil rather than the receiver, so the next call in a chain is sent to nil and the line raises NoMethodError. That is the whole trap in when upcase! returns nil mid-chain and in chomp! returning nil when nothing changes, which are the same behaviour chained two different ways.

The third question inverts the expectation. The Ruby core documentation describes FrozenError as "Raised when there is an attempt to modify a frozen object", which is what most people expect str += "def" to raise when str is frozen. Nothing is raised, and the Ruby syntax documentation on assignment says why: under Abbreviated Assignment it states that a += 2 "is equivalent to" a = a + 2. A += is not a mutation. It builds a new String and binds it to the same name, and the frozen object is never written to. Calling += on a frozen String has the sequence.

What the group tests is whether a candidate separates mutating an object from rebinding a name. That distinction is what decides whether a change is visible to everything else holding a reference to the same object, which is the shape most aliasing bugs take.

Blocks, procs and lambdas

Five questions, one subject: what a block is, what it can see, and what leaving one does.

The Ruby core documentation for Proc states the return rule outright. "In lambdas, return and break means exit from this lambda", while "In non-lambda procs, return means exit from embracing method (and will throw LocalJumpError if invoked outside the method)". So a lambda's return hands a value back to the line that called it, and a proc's return ends the method around it, which is the entire question in return inside a lambda vs inside a proc.

break carries a value of its own. The Ruby syntax documentation on control expressions states that "break accepts a value that supplies the result of the expression it is 'breaking' out of", which is what giving break a return value turns on.

Scope is settled on one documentation section. Under Local Variable Scope, the assignment page states both halves: "Variables defined in an outer scope appear inner scope", and "Since the block creates a new scope, any local variables created inside it do not leak to the surrounding scope". A block therefore reads and writes the locals around it, which is what blocks reaching outer local variables demonstrates. The opt-out is documented on the calling methods page under Block Local Arguments: "Assigning to a block-local argument will not override local arguments outside the block in the caller's scope".

Composition closes the group. Proc#>> "Returns a proc that is the composition of this proc and the given g. The returned proc takes a variable number of arguments, calls this proc with them then calls g with the result", and Proc#<< carries the same sentence with g called first. Composing procs with << and >> chains four of them, so answering it is a test of applying that sentence rather than recalling a diagram.

Two yields and one block

A method that yields twice runs the block twice, and two yields, one block is built on exactly that: n += yield written twice against a block returning 42.

The Ruby syntax documentation on methods documents yield and recommends it, saying that "if you are only going to call the block and will not otherwise manipulate it or send it to another method, using yield without an explicit block parameter is preferred", and it shows a single-yield example. That a method may yield repeatedly, running the block once per call, is not spelled out there. It is what every Enumerable method depends on, and the runnable answer demonstrates it.

What the question tests is whether a candidate treats a block as a value that can be called many times, rather than as a piece of syntax attached to one call.

Scope and method lookup

Four questions about where a name resolves, and the sharpest of them is settled by a single sentence. The Ruby syntax documentation on assignment states that "the local variable is created when the parser encounters the assignment, not when the assignment occurs". A lastname = "Snow" if false never executes, and still defines lastname as a local for the rest of the method, so the later reference finds a local holding nil instead of calling the method of that name. The same section describes the mirror-image failure: "Since ruby parses the bare a left of the if first and has not yet seen an assignment to a it assumes you wish to call a method". An assignment behind if false still shadows your method is that rule in five lines.

for is the second. The Ruby syntax documentation on control expressions states that "the for loop is similar to using each, but does not create a new variable scope", and adds that "the for loop is rarely used in modern ruby programs". The loop variable is therefore the surrounding method's own variable and keeps its last value after the loop ends, which is why for i in (1...10) clobbers your i.

Argument rebinding is the third, and it needs care with its source. Reassigning a method argument never touches the caller is true, but the one documentation sentence on the subject reads like the opposite: the calling methods page says "all arguments in ruby are passed by reference and are not lazily evaluated". Both are correct because they are about different things. The caller and the method share the object, and a += [4] inside the method does not write to that object: by the assignment page's rule, it binds a new array to the method's own local name. The object is shared; the name is not.

alias and alias_method across inheritance

alias and alias_method look interchangeable until a subclass is involved, which is what alias vs alias_method across inheritance puts on the table.

The Ruby documentation covers the surrounding mechanics well. On method lookup, the calling methods page states that "when you send a message, Ruby looks up the method that matches the name of the message for the receiver. Methods are stored in classes and modules so method lookup walks these, not the objects themselves", and then gives the order: prepended modules in reverse order, a matching method on the class itself, included modules in reverse order, then up the superclass chain. On the keyword, the miscellaneous syntax page states that "the alias keyword is most frequently used to alias methods" and that "you may use alias in any scope". On the method, Module#alias_method "makes new_name a new copy of the method old_name", which "can be used to retain access to methods that are overridden".

The contrast itself is the part to be careful with. The common explanation is that the keyword resolves against its lexically enclosing class at parse time while the method call dispatches on self at runtime. That explanation is not stated in the Ruby documentation, so treat it as the prevailing account rather than a documented rule, and lean on the behaviour the runnable answer demonstrates rather than on the wording.

Equality and identity

Two questions separate three operations that look like one.

The Ruby core documentation for Object draws the line between == and eql? and gives the example inline: "Numeric types, for example, perform type conversion across ==, but not across eql?", followed by 1 == 1.0 #=> true and 1.eql? 1.0 #=> false. The same page says equal? "should never be overridden by subclasses as it is used to determine object identity", which makes the three a ladder rather than three synonyms: value with conversion, value and type, then the object itself. 1.0 == 1 versus eql? and equal? walks the ladder.

Class membership is the second. Object#instance_of? "returns whether self is an instance of the given class", and the documentation carries the inheritance case as a worked example on the page itself: with class A; end, class B < A; end and class C < B; end, it shows b.instance_of? A #=> false, b.instance_of? B #=> true and b.instance_of? C #=> false. Exact class, no ancestors, which is what separates it from is_a? and kind_of? and what instance_of? ignoring inheritance tests.

Both questions test the same habit. A candidate who reaches for == everywhere and a candidate who picks between ==, eql? and equal? write different Hash keys and different comparison methods, and the difference shows up long before any interview does.

Hash defaults

Two questions, and the Ruby core documentation answers both in its own words.

Under Default Values, the Hash page warns: "Note that the default value is used without being duplicated. It is not advised to set the default value to a mutable object". One array is created when Hash.new([]) is evaluated, and every missing key is handed that same array, so pushing onto h[:a] is visible through h[:b] and the hash itself is still empty because nothing was ever stored under a key. The shared default array in Hash.new([]) is that paragraph as code.

The block form fails in the opposite direction. Under Default Proc, the same page states that "when the default proc for a Hash is set (i.e., not nil), the default value returned by method [] is determined by the default proc alone", and notes that "setting the default proc will clear the default value and vice versa". Hash.new { [] } therefore returns a brand new array on every miss, and because the block never assigns anything, that array is discarded the moment the push finishes. Why Hash.new { [] } stays empty after << is the result.

What both questions test is whether a candidate knows that a default is a fallback for a read, not an insertion. The working form assigns inside the block, as in Hash.new { |hash, key| hash[key] = [] }, and a candidate who can write that has understood the two failures above rather than memorised them.

Precedence and truthiness

Three questions about what binds to what, and the first is answered by the table rather than by a paraphrase of it. The Ruby syntax documentation on precedence lists the operators "from highest to lowest", and in that list && sits one row above ||, both sit above the row for =, +=, -=, etc., which in turn sits above not and above or, and. So x || y && "Rails" groups as x || (y && "Rails"), which is && binding tighter than ||.

Truthiness is the second. The control expressions page states that "for the tests in these control expressions, nil and false are false-values and true and any other object are true-values", so exactly two values are falsy and everything else, including 0 and "", is not. The other half of the question is that ! is a real method: the Ruby core documentation for BasicObject documents BasicObject#! as "boolean negate", which makes !! two method dispatches rather than a syntax form. !! is two calls to BasicObject#! uses the explicit .!@ spelling to make that visible.

Multiple assignment closes the group. The Ruby syntax documentation on assignment states that "you can use multiple assignment to swap two values in-place", under the section that first establishes that "you can assign multiple values on the right-hand side to multiple variables", which is why swapping two variables with a, b = b, a needs no temporary.

Enumerable and laziness

Two questions about when work happens, rather than about what an iterator returns.

The Ruby core documentation describes Enumerator::Lazy as "a special type of Enumerator, that allows constructing chains of operations without evaluating them immediately, and evaluating values on as-needed basis", and adds that "in order to do so it redefines most of Enumerable methods so that they just construct another lazy enumerator". The page states the purpose too: "this class allows idiomatic calculations on long or infinite sequences, as well as chaining of calculations without constructing intermediate arrays". Evaluating per element rather than per stage is the direct consequence, and it is what changes the order of side effects in lazy enumerators running the whole chain per element: each element is carried through every block before the next element starts.

The second question is smaller and reuses the truthiness rule above. Object#itself is documented in one line, "returns self", so select(&:itself) keeps an element when the element is itself truthy. Combined with the control expressions rule that only nil and false are false-values, that means 0, "" and [] all survive the filter, which is the point of filtering truthy values with select(&:itself).

Laziness is the subject in this list with the most direct consequence for a running application, because the documented reason the class exists is chaining "without constructing intermediate arrays". On a large collection the same chain written eagerly materialises one array per stage, which is memory a lazy chain never asks for.

Rails specifics

Two questions leave Ruby for the framework, and both are documented by the Rails Guides and by the framework source at the same time.

Rendering twice is the first. The Rails Guides section on avoiding double render errors names the message, "Can only render or redirect once per action", and shows the exact shape that produces it: a conditional render followed by an unconditional one, with no return between them. The framework source says what is raised. In Rails 8.1.3.1, actionpack/lib/action_controller/metal/rendering.rb opens render with raise ::AbstractController::DoubleRenderError if response_body, and AbstractController::DoubleRenderError is declared in actionpack/lib/abstract_controller/rendering.rb with a default message that spells out the cause: "Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action". The Guide quotes the shorter message and the constant carries the longer one, so a reader may meet either text, but the class raised is the same one. Two render calls in one Rails action is that controller.

Key collision is the second. The Rails Guides section on symbolize_keys states that the method "returns a hash that has a symbolized version of the keys in the receiver, where possible", by "sending to_sym to them", and then gives the collision case verbatim: "In case of key collision, the value will be the one most recently inserted into the hash", with the example { "a" => 1, a: 2 }.symbolize_keys # => {:a=>2}. The implementation in activesupport/lib/active_support/core_ext/hash/keys.rb is a one-line transform_keys { |key| key.to_sym rescue key }, which is why the last write wins rather than the first. When symbolize_keys collides :key and "key" puts a symbol and a string key in the same literal.

Sources

  1. Ruby core documentation, String (Ruby 3.4)
  2. Ruby core documentation, FrozenError (Ruby 3.4)
  3. Ruby core documentation, Proc (Ruby 3.4)
  4. Ruby core documentation, Object (Ruby 3.4)
  5. Ruby core documentation, BasicObject (Ruby 3.4)
  6. Ruby core documentation, Module (Ruby 3.4)
  7. Ruby core documentation, Hash (Ruby 3.4)
  8. Ruby core documentation, Enumerator::Lazy (Ruby 3.4)
  9. Ruby syntax documentation, Assignment (Ruby 3.4)
  10. Ruby syntax documentation, Control Expressions (Ruby 3.4)
  11. Ruby syntax documentation, Calling Methods (Ruby 3.4)
  12. Ruby syntax documentation, Methods (Ruby 3.4)
  13. Ruby syntax documentation, Miscellaneous Syntax (Ruby 3.4)
  14. Ruby syntax documentation, Precedence (Ruby 3.4)
  15. Ruby on Rails Guides, Layouts and Rendering in Rails
  16. Ruby on Rails Guides, Active Support Core Extensions
  17. Rails framework source, actionpack/lib/abstract_controller/rendering.rb (tag v8.1.3.1)
  18. Rails framework source, actionpack/lib/action_controller/metal/rendering.rb (tag v8.1.3.1)
  19. Rails framework source, activesupport/lib/active_support/core_ext/hash/keys.rb (tag v8.1.3.1)

Back to Ruby on Rails jobs: what the data actually says