Instance variable '@somevar' is initialized inside a begin-rescue, so it can potentially be left uninitialized if an exception is raised and rescued

Simple question that probably someone on the internet may have on the future. Why does this happen? Is the compiler not able to detect that I’m exiting the program using exit(1)?

In this example @somevar is @redis.

def initialize
  begin
    @redis = Redis::PooledClient.new(unixsocket: CONFIG.redis_socket || nil, url: CONFIG.redis_url || nil)
    if @redis.ping
      LOGGER.info "Connected to Redis compatible DB via unix domain socket at '#{CONFIG.redis_socket}'" if CONFIG.redis_socket
      LOGGER.info "Connected to Redis compatible DB via TCP socket at '#{CONFIG.redis_url}'" if CONFIG.redis_url
    end
  rescue ex
    LOGGER.info "Failed to connect to a Redis compatible DB: #{ex.message}"
    exit(1)
  end
end
Error: instance variable '@redis' of Invidious::Database::Videos::CacheMethods::Redis_ must be Redis::PooledClient, not Nil

Instance variable '@redis' is initialized inside a begin-rescue, so it can potentially be left uninitialized if an exception is raised and rescued

The intended way to handle this is letting the program crash by itself without begin?

Does it work if you don’t exit(1) but instead raise ex and let that bubble up? it doesn’t. My guess is the compiler just isn’t taking the exit/raise into account.

But maybe it would be a better design to pass in the redis client to use to the constructor vs doing that within the constructor itself? Then your error handling wouldn’t be intertwined with instantiation of the obj itself.

The Redis client will not be used outside that class at all, so I prefer to connect to it using the constructor. I guess that makes sense right?

I guess it depends on your background. I come from a more Java-like background so my preference is to rely on Dependency Inversion so something along the lines of:

def initialize(@client : Redis::ClientInterface); end

Where the client you provide it is already validated to be good. This makes it easy to test the type in that you could easily provide it a mock client.

But in your case maybe just remove the begin/rescue and handle logging failures higher up? This way obj instantiation would still fail if redis fails to connect or whatever, but the compiler can still guarantee it won’t be nilable.

1 Like

Reduced:

class Foo
  def initialize
    begin
      @foo = 1
    rescue exc
      raise exc
    end
  end
end

Foo.new # Error: instance variable '@foo' of Foo must be Int32, not Nil
        # Instance variable '@foo' is initialized inside a begin-rescue, so it can potentially be left uninitialized if an exception is raised and rescued

It seems the compiler doesn’t correctly consider that the rescue block may not return control flow to the outer scope when considering this.

It does so in other cases, though. For example as a workaround you could pull out the ivar an assign the value of the begin-rescue block. That’ll require an explicit type type declaration; the compiler can’t infer it.

class Foo
  @foo : Int32
  def initialize
    @foo = begin
      1
    rescue esc
      raise esc
    end
  end
end

Foo.new
3 Likes