Today I was doing a quick code review on a pull request and noticed that all of our test suite had failed CI. This was surprising since the change made was so small and looked so reasonable.
This is (more or less) what the method had been:
def query @query ||= begin query = SomeModel.where(...) query = query.addressed if addressed query end end
The details of
addressed are irrelevant to this article, so don’t worry about them. I’m leaving it here to illustrate that the
query variable may have been updated under some circumstances.
The method had been updated like this in the PR:
def query @query ||= begin query = SomeModel.where(...) query = query.addressed if addressed return query.to_a end end
So, all that happened was that the last line of the
begin block had been changed from
return query.to_a. It’s not important to get into the reasons why the query was being turned into an array, it just was.
However, this caused all of our tests to fail with a “void value expression (SyntaxError)”.
At first I didn’t see what was going on, but, after some research, I figured it out. Long story short, this would work just fine without the
It seems natural to think of a
begin block as an inline method that has its own return value, but, in reality, it’s an expression that evaluates to a value. There’s a subtle difference. Consider this expression:
5 + 5
This expression evaluates to 10. It’s not a method that returns a value. If you run that in a Ruby console you’ll see as much.
This is not a valid expression in Ruby, outside the context of a method:
return 5 + 5
Running this in a console gives you an “unexpected return” error.
Next, let’s consider this method:
def foo 5 + 5 end
This method, when evaluated, returns the result of evaluating the expression
5 + 5. The reason it returns this is because Ruby assumes that the last expression evaluated in any method is its return value, unless something else is returned first.
The above is exactly the same as this next example, we’re just being explicit now:
def foo return 5 + 5 end
It’s important to know that the
return keyword immediately terminates execution of the method and returns the specified value (or
nil). Here’s an example with an early return:
def foo return 75 5 + 5 end
As you might expect, this returns
75 due to the early return terminating execution and returning the specified value.
Now let’s consider variable assignments:
def foo a = 5 + 5 return a end
Running this method returns
10. Keep in mind that the
return statement is immediately terminating the method and returning the value of
Here’s another variable assignment example:
def foo(bool) if bool a = 5 + 5 else a = 75 end return a end
In this example we’re setting
a to 10 or 75 depending on a conditional statement and then returning
a. This is one way to write this logic. We could also have written it this way:
def foo(bool) a = if bool 5 + 5 else 75 end return a end
This works because
if statements in Ruby (like
case statements, blocks, etc) evaluate to a value. It’s tempting to think of them as returning a value, but that’s not what’s going on. If they returned, the
foo method would immediately terminate and return the value of the statement. To put it another way,
a is set to the result of evaluating the entire conditional statement.
But what about this example?
def foo(bool) a = if bool 5 + 5 else return 75 end return a end
If you try to paste this into a Ruby console you’ll get a “void value expression” error. But why? Isn’t it effectively the same as the example above? Nope! Ruby sees the
a = and knows it’s expected to assign something to
a. The “something” being assigned is the result of evaluating the expression to the right of the
It’s pretty clear what this should be if
true, but what if it’s
false? Our instinct might be to assume that the
if statement returns a value, but, as we determined above, that’s not how
Effectively, Ruby sees that it could be in the middle of setting
a and be interrupted to return
foo, effectively leaving
a abandoned. That is, the expression to the right of the
= operator either evaluates to a value or not at all (IE: a void value). Ruby doesn’t like this and raises a “void value expression” error.
So, looking back at the
query method above, it’s trying to set
@query to the result of evaluating the
begin ... end expression. But, before it can do so, it encounters a
return statement that wants to immediately exit the
query method, which Ruby dislikes.
The solution to this problem is simple: Just remove the
return keyword. Since Ruby assumes that a statement’s value is the last line of code evaluated, Ruby will set
@query to the result of evaluating