Ruby’s timeout method operates by raising a exception, Timeout::Error. Timeout::Error inherits from Interrupt, SignalException, and finally Exception. In particular, it doesn’t inherit from StandardError, which means it isn’t caught by a default rescue clause with no exception class specified. When I first encountered this, I was annoyed that I had to explicitly catch timeout errors anytime I wanted to recover from errors in general, whether they were timeout errors or not. I realized something the other day that changed my opinion on this however.

Here’s the timeout method for Ruby 1.8.6.


  def timeout(sec, exception=Error)
    return yield if sec == nil or sec.zero?
    raise ThreadError, "timeout within critical session" if Thread.critical
    begin
      x = Thread.current
      y = Thread.start {
        sleep sec
        x.raise exception, "execution expired" if x.alive?
      }
      yield sec
      #    return true
    ensure
      y.kill if y and y.alive?
    end
  end

It operates by starting a thread, y that will raise an exception after sleeping. It raises the exception by calling x.raise, causing the exception be raised in thread x, which is the thread that called timeout—the one doing the work that needs to be timed out. That exception terminates the execution of the thread wherever it happens to be and starts propagating an exception up the stack. This exception is subject to the same handling as every other exception that might happen. It can caught be a rescue clause. In particular, it can be caught by a rescue clause that is within the block being subjected to the timeout


  require 'timeout'

  begin
    Timeout.timeout(1) do
      begin
        sleep 2
      rescue Exception => e
        puts "Caught #{e.inspect} within timeout" 
      end
    end
  rescue Exception => e
    puts "Caught #{e.inspect} at top level" 
  end

This produces the output Caught #<Timeout::Error: execution expired> within timeout. If Timeout::Error extended from StandardError, it would be caught by any generic rescue expression that would handle exceptions occurring at the point where the timeout error happened to be raised. This rescue clause may not even be in your application code—it could be in a library or the Rails framework, which could result in very hard to track down bugs. We can verify this is the case by specifying what type of error timeout should raise.


  require 'timeout'

  begin
    Timeout.timeout(1, StandardError) do
      begin
        sleep 2
      rescue => e
        puts "Caught #{e.inspect} within timeout" 
      end
    end
  rescue => e
    puts "Caught #{e.inspect} at top level" 
  end

The only differences here are that Timeout.timeout is passed the type of exception to raise, StandardError, and that the rescue clauses have be changed to omit the exception type. The output is essentially the same as before, Caught #<StandardError: execution expired> within timeout.

Incidentally, SystemTimer terminates the offending thread by raising an exception as well. If there is a rescue clause that handles Timeout::Error within the block given for either Timeout or SystemTimer, it could prevent the error from reaching handling code outside the timeout call. Fortunately the likelihood of encountering this scenario is minimized because Timeout::Error doesn’t inherit from StandardError. I haven’t hit a bug caused by this in practice yet. I’m sure if I had before I had thought through the situation it would have taken me a lot longer to figure out what was going on.

Sorry, comments are closed for this article.