Jake Goulding

Using Ruby blocks to ensure resources are cleaned up

In programming, cleaning up resources you have created is an easily-overlooked problem. In languages like C, you have to clean up everything by hand: memory, files, network sockets, etc. Languages that have a garbage collector take away the need to explicitly free memory, but you still have to manage the other resources.

In Ruby, we can use blocks to help ensure resources are closed. You’ve probably seen this idiom when dealing with files. The File class ensures that the file is closed after the block is finished:

1
2
3
File.open('file.txt') do |file|
  # ... work with the file ...
end

However, this can only be used when the lifespan of the resource is one method call. If you need to keep the file around in an instance variable, then you cannot use this pattern, and must fall back to explicitly closing the resource:

1
2
3
4
5
6
7
8
9
class Foo
  def initialize
    @file = File.open('file.txt')
  end

  def close
    @file.close
  end
end

To make this code nicer on yourself and others that have to use it, you should add a method that handles closing the resource for you, just like File does:

1
2
3
4
5
6
7
8
class Foo
  def self.open
    foo = Foo.new
    yield foo
  ensure
    foo.close
  end
end

Unfortunately, this clean implementation of open has a problem if the constructor of File can throw an exception. The exception will occur before the variable foo can be set to anything, so the close message will be sent to nil instead, causing another exception! This certainly didn’t happen in any code I was writing…

To handle this case, we have to give up on using the implicit `begin“ block from the function and create our own scope:

1
2
3
4
5
6
7
8
9
10
class Foo
  def self.open
    foo = Foo.new
    begin
      yield foo
    ensure
      foo.close
    end
  end
end

Now we can safely create a Foo and close it all in one place, but what if another object wants to keep an instance of Foo around for longer? It’s nice to transparently handle both cases:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Foo
  def self.open
    foo = Foo.new

    return foo unless block_given?

    begin
      yield foo
    ensure
      foo.close
    end
  end
end