From: "ioquatix (Samuel Williams)" Date: 2021-08-19T09:57:40+00:00 Subject: [ruby-core:105005] [Ruby master Feature#18083] Capture error in ensure block. Issue #18083 has been updated by ioquatix (Samuel Williams). @matz I agree with your general feeling completely. But it's too late: We already have "ugly and against the basic concept of ensure" in the form of `$!`. As demonstrated, people are checking `$!` and unfortunately, in general code, it is not safe to check `$!` in an `ensure` block because it might come from the parent scope (or some other scope). This puts us in a difficult situation. I think the decision depends on whether you want to support or deprecate the usage of `$!`. Fundamentally, if you think that `ensure => error` is ugly, I believe you must agree `ensure ... if $!` is equally ugly. ## Support `$!` If we want to support $!, I would propose that we make it safe, by limiting it to the current block. That means it's effectively reset on entering a `begin...ensure...end` block, so we prevent it carrying state from the parent scope (if any). ## Deprecate `$!` If we want to deprecate $!, I would propose that we consider how we want engineers to approach the problem they are currently depending on `$!` for. The most common use case is something like this: ```ruby begin resource = acquire # get a resource from the pool or allocate one if pool is empty. yield resource ensure if $! free(resource) # resource is broken, get rid of it. else release(resource) # resource is okay, return it to the pool. end end ``` The proposal to use `else` is a good one, except it doesn't capture all flow controls: ```ruby begin resource = acquire # get a resource from the pool or allocate one if pool is empty. yield resource rescue Exception free(resource) # resource is broken, get rid of it. else release(resource) # resource is okay, return it to the pool. end ``` The problem with the above code, is there are some cases where you can exit the block and neither the rescue nor the else block are executed. It will result in a resource leak. Also, some people would argue that `rescue Exception` is wrong. So, can we make the above better? If we make `else` the same as "ensure without exception" then it would be sufficient, but right now it's not. The next best alternative is something like this: ```ruby begin resource = acquire # get a resource from the pool or allocate one if pool is empty. yield resource rescue Exception => error free(resource) # resource is broken, get rid of it. raise ensure unless error release(resource) # resource is okay, return it to the pool. end end ``` So the fundamental problem we have is that we don't have any way to guarantee, AFAIK, on error execute this code, otherwise execute this other code, and it turns out that a lot of code needs this, essentially "ensure if no exception was raised" or "ensure if no rescue block was executed", or something similar. By the way, your proposed solution also falls short: ~~~ catch(:finished) do begin throw :finished no_error = true ensure p :error_handling unless no_error end end ~~~ Will execute `error_handling`. This is why it's a non-trivial problem. Everyone comes up with the wrong answer (including me). If you look at real code, you see these incorrect patterns repeated over and over. ---------------------------------------- Feature #18083: Capture error in ensure block. https://bugs.ruby-lang.org/issues/18083#change-93405 * Author: ioquatix (Samuel Williams) * Status: Open * Priority: Normal ---------------------------------------- As discussed in https://bugs.ruby-lang.org/issues/15567 there are some tricky edge cases. As a general model, something like the following would be incredibly useful: ``` ruby begin ... ensure => error pp "error occurred" if error end ``` Currently you can get similar behaviour like this: ``` ruby begin ... rescue Exception => error raise ensure pp "error occurred" if error end ``` The limitation of this approach is it only works if you don't need any other `rescue` clause. Otherwise, it may not work as expected or require extra care. Also, Rubocop will complain about it. Using `$!` can be buggy if you call some method from `rescue` or `ensure` clause, since it would be set already. It was discussed extensively in https://bugs.ruby-lang.org/issues/15567 if you want more details. -- https://bugs.ruby-lang.org/ Unsubscribe: