From: "ioquatix (Samuel Williams)" Date: 2021-08-19T08:04:43+00:00 Subject: [ruby-core:105001] [Ruby master Feature#18083] Capture error in ensure block. Issue #18083 has been updated by ioquatix (Samuel Williams). We want to make it easy for people to write robust Ruby programs, even if there is failure. As discussed in there are some tricky edge cases when using `$!` because it's non-local state. As an example: ``` ruby def do_work begin # $! may be set on entry to `begin`. # ... do work ... ensure if $! puts "work failed" else puts "work success" end end end begin raise "Boom" ensure # $! is set. do_work end ``` There are real examples of this kind of code: https://bugs.ruby-lang.org/issues/15567#note-26 There is another kind of error that can occur: ``` ruby begin raise "Boom" rescue => error # Ignore? ensure pp error # pp $! # nil end ``` As a general model, something like the following would be incredibly useful, and help guide people in the right direction: ``` ruby begin ... ensure => error pp "error occurred" if error end ``` Currently you can almost achieve this: ``` ruby begin ... rescue Exception => error # RuboCop will complain. 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 (e.g. care with `rescue ... => error` naming. Also, Rubocop will complain about it, which I think is generally correct since we shouldn't encourage users to `rescue Exception`. Because `$!` can be buggy and encourage incorrect code, I also believe we should consider deprecating it. But in order to do so, we need to provide users with a functional alternative, e.g. `ensure => error`. Another option is to clear `$!` when entering `begin` block (and maybe restore it on exit?), but this may break existing code. #### More Examples ``` Gem: bundler has 1480241823 downloads. /lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb ensure return sleep_time unless $! Gem: rubygems-update has 817517436 downloads. /lib/rubygems/core_ext/kernel_require.rb ensure if RUBYGEMS_ACTIVATION_MONITOR.respond_to?(:mon_owned?) if monitor_owned != (ow = RUBYGEMS_ACTIVATION_MONITOR.mon_owned?) STDERR.puts [$$, Thread.current, $!, $!.backtrace].inspect if $! Gem: launchy has 196850833 downloads. /lib/launchy.rb ensure if $! and block_given? then yield $! Gem: spring has 177806450 downloads. /lib/spring/application.rb ensure if $! lib = File.expand_path("..", __FILE__) $!.backtrace.reject! { |line| line.start_with?(lib) } Gem: net-http-persistent has 108254000 downloads. /lib/net/http/persistent.rb ensure return sleep_time unless $! Gem: open4 has 86007490 downloads. /lib/open4.rb ensure killall rescue nil if $! Gem: net-ssh-gateway has 74452292 downloads. /lib/net/ssh/gateway.rb ensure close(local_port) if block || $! Gem: vault has 48165849 downloads. /lib/vault/persistent.rb ensure return sleep_time unless $! Gem: webrick has 46884726 downloads. /lib/webrick/httpauth/htgroup.rb ensure tmp.close if $! Gem: stripe-ruby-mock has 11533268 downloads. /lib/stripe_mock/api/server.rb ensure raise e if $! != e Gem: logstash-input-udp has 8223308 downloads. /lib/logstash/inputs/udp.rb ensure if @udp @udp.close_read rescue ignore_close_and_log($!) @udp.close_write rescue ignore_close_and_log($!) Gem: shrine has 6332950 downloads. /lib/shrine/uploaded_file.rb ensure tempfile.close! if ($! || block_given?) && tempfile ``` Discussion: * nobu: `$!` is thread-local. * ko1: Your proposal is to make shorthand for the following code? * mame: No ```ruby= begin ... ensure error = $! ... end ``` * ko1: FYI ```ruby= def foo p $! #=> # raise SyntaxError rescue SyntaxError end begin raise 'FOO' ensure foo; p $! #=> # exit end ``` * mame: It is surprising to me that `$!` is not a method-local but a thread-local variable ```ruby= at_exit do # |exception| ??? if $! # exception puts "Exiting because of error" end end ``` ```ruby= begin raise "Boom" ensure begin ensure => error # should be nil - yes correct. This is the difference between ko1's workaround and samuel's proposal p $! #=> # error = $! puts "failed" if $! end end ``` * knu: ```ruby= begin Integer("foo") rescue begin Integer("1") ensure => error # You can't tell `$!` came from this begin block without being rescued. p $! #=> # p error #=> nil end end ``` You can almost achieve the ideal situation with this: ``` ruby begin ... rescue Exception => error # RuboCop will complain. raise ensure pp "error occurred" if error end ``` ---------------------------------------- Feature #18083: Capture error in ensure block. https://bugs.ruby-lang.org/issues/18083#change-93400 * 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: