The amazing adventures of Doug Hughes

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 query to 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 return keyword.

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 a.

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 = operator.

It’s pretty clear what this should be if bool is 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 return works.

Effectively, Ruby sees that it could be in the middle of setting a and be interrupted to return 75 from 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 query.to_a.

Comments on: "Ruby Void Value Expression Errors Explained" (1)

  1. Thanks for the explanation. Now I understand it totally. Mom!!

    > WordPress.com

    Like

Comments are closed.

Tag Cloud