[ruby-core:105001] [Ruby master Feature#18083] Capture error in ensure block.
From:
"ioquatix (Samuel Williams)" <noreply@...>
Date:
2021-08-19 08:04:43 UTC
List:
ruby-core #105001
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 <https://bugs.ruby-lang.org/issues/15567> 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 # <RuntimeError: Boom>
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 $! #=> #<RuntimeError: FOO>
raise SyntaxError
rescue SyntaxError
end
begin
raise 'FOO'
ensure
foo; p $! #=> #<RuntimeError: FOO>
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 $! #=> #<RuntimeError: Boom>
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 $! #=> #<ArgumentError: invalid value for Integer(): "foo">
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: <mailto:ruby-core-request@ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>