Cumbersome scoping rules for try - catch - finally

I just had to implement the following logic:

1 - connect to DB (with potential error)
2 - run query (with potential error)
3 - return query result
4 - make sure the connection is closed

The most concise way to write this would be:

try # catch all errors here
  conn = db_connect(...)
  result = db_query(conn, ...)
finally # make sure you disconnect
  disconnect(conn)
end

do_stuff_with_query_result(result)

But due to the scoping rules, you end up with something like:

conn = db_connect(...) # this will require another try - catch if you want to handle the exception
result = try 
           db_query(conn, ...)
         finally
           disconnect(conn)
         end

do_stuff_with_query_result(result)

It seems unnecessarily complicated. Is there any other way to handle this?

Also, why isn’t a variable defined inside try available inside finally ? I expected that the whole try ... end is just one scope block.

julia> try
         a = 2+2
       finally
         @show a
       end
------ UndefVarError ------------------- Stacktrace (most recent call last)

 [1] — anonymous at <missing>:?

 [2] — macro expansion; at REPL[1]:4 [inlined]

 [3] — macro expansion; at show.jl:218 [inlined]

UndefVarError: a not defined
5 Likes

If variables that are created inside the try block are available inside the finally block, then that implicitly assumes that part of the code inside the try block will run successfully. But the point of try is that code inside it might fail…

4 Likes

It’s similar to result in my first example. It is initialised but if the try fails, it will reference something else (whatever the catch will return).

And that needs to be checked after the try block. So it just moves the check somewhere else.

You can declare the variable as local to make it visible inside the finally.

function foo()
    local yy
    try 
      yy = 2
    finally 
      @show yy
    end
end
foo()

I agree with @Per: if db_connect fails, conn will be undefined and your finally will fail.

You could create a new method for db_connect that handles the try-catch-disconnect.

Something like:

db_connect(f::Function, args...) = begin
  conn = db_connect(args...)
  try
    f(conn)
  finally
    disconnect(conn)
  end
end

(Something like that. I didn’t test this.)

With this method you can then use a do-block:

db_connect(...) do conn
  result = db_query(conn, ....)
end

and this will be guaranteed to close the connection even if the db_query throws an exception.

You can then wrap that last piece of code in another try block if you want.

Edit: There was a better example in the documentation. Fixed my code.
https://docs.julialang.org/en/stable/manual/functions/#Do-Block-Syntax-for-Function-Arguments-1

Edit2: I guess there might still be issues with the scope of the result variable.

1 Like

Yes, that’s a nice design pattern, it’s a good approach for sure.

Heh, indeed, result would have to be defined before db_connect in order to be accessible after it, which would move the problem :slight_smile:

Thus better:

result = db_connect(...) do conn
  db_query(conn, ....)
end

This could be combined with another design pattern where the try ... catch block would return a Nullable type which isnull on error.

I often handle this problem by making an zero or empty value for result before doing the try catch finally dance.

   result = QueryResult()
   conn = DBConnection()
   try
      conn = connect(params)
      res = query(conn, "select fields from tables")
   finally
     close(conn)
   end
   return res

Then when you call it you check that res != QueryResult() to make sure that the query ran successfully.
This only works for types that have a valid “uninitialized” state. If there is no such “invalid” value, then you can use something like https://github.com/iamed2/ResultTypes.jl. Where you return the value that contains the data and another value of a Error type to indicate failure.

1 Like

As a final option, you can use local conn, result to introduce the variables into the outer scope (even without a definition). They’re then inherited into the try/catch/finally scopes. This has the downside of an undefined variable error if db_connect fails before it returns a value for conn.

Which you can catch with a try-catch block :joy_cat:. It’s try-catch blocks all the way down… :turtle: :turtle: :turtle:

4 Likes

You probably meant

res = QueryResult() # instead of result = ... 

That’s going to be harder to check though (does QueryResult have an isempty() method or how do you know it worked). It’s maybe more Julian to say

res = Nullable{QueryResult}() 

and use isnull(res) to check.

Yeah, once you start going down the rabbit hole it gets crazy fast. :smile:

Luckily, with the notable exception of GUI apps, DB connection errors are usually allowed to bubble all the way up and crash the app since you can’t do anything without the DB handle.