I have found myself in need to execute a string. Current method is to use the Kernel#eval() method. Everything is working fine, but my error handling isn't working. For example, a missing closing quotation mark will completely kill and exit the program.
Here's an excerpt. Any idea why I can't catch the error?
def process(str)
print "\n"
eval(str)
rescue => e
puts e
end
>> process('"')
console.rb:90:in `eval': (eval):1: unterminated string meets end of file (SyntaxError)
from console.rb:90:in `process'
from console.rb:81:in `bouncer'
from console.rb:14:in `block in prompt'
from console.rb:11:in `loop'
from console.rb:11:in `prompt'
from console.rb:97:in `<main>'
According to the documentation:
A rescue clause without an explicit Exception class will rescue all StandardErrors (and only those).
SyntaxError is not a StandardError. To catch it, you have to be explicit, e.g.:
def process(str)
print "\n"
eval(str)
rescue Exception => e
puts e
end
process('"')
Output:
(eval):1: unterminated string meets end of file
Related
Given the following code:
test.rb
require 'csv'
def meth1
meth2
end
def meth2
begin
iter = CSV.foreach('').each # empty file path, will raise exception.
iter.next
rescue Exception => e
puts e
puts e.backtrace
end
end
meth1
Two questons.
First, why is the backtrace truncated and not showing meth1 or meth2 calls:
No such file or directory # rb_sysopen -
ruby test.rb
/Users/x/.rbenv/versions/2.7.2/lib/ruby/2.7.0/csv.rb:641:in `initialize'
/Users/x/.rbenv/versions/2.7.2/lib/ruby/2.7.0/csv.rb:641:in `open'
/Users/x/.rbenv/versions/2.7.2/lib/ruby/2.7.0/csv.rb:641:in `open'
/Users/x/.rbenv/versions/2.7.2/lib/ruby/2.7.0/csv.rb:510:in `foreach'
test.rb:in `each'
Second, the (truncated) backtrace points to line 641 of CSV (using Ruby v 2.7.2). However line 641 has no initialize() method. Where is this coming from?
.rbenv/versions/2.7.2/lib/ruby/2.7.0/csv.rb
...
begin
f = File.open(filename, mode, **file_opts) ## line 641
rescue ArgumentError => e
raise unless /needs binmode/.match?(e.message) and mode == "r"
mode = "rb"
file_opts = {encoding: Encoding.default_external}.merge(file_opts)
retry
end
If I try another test calling File.open('') directly (instead of through CSV), the resulting backtrace shows everything as expected (including the calls to meth1 and meth2).
Any Ruby gurus out there know what is going on?
I'm not sure I understand; #foreach:510 calls open, #open:641 calls File.open.
In any case, Ruby backtraces have always been a bit wonky, particularly with local top-level files.
The reason you see the initialize is because foreach is a class method of CSV, so there's some behind-the-scenes Ruby shenanigans.
You can use the private method caller_locations, however:
puts e.send(:caller_locations)
which outputs:
test.rb:7:in `meth2'
test.rb:4:in `meth1'
test.rb:20:in `<main>'
This is just the file's top level methods.
You can play games like ruby -d that'll at least get you the line of the script:
Exception `LoadError' at /Users/dave/.asdf/installs/ruby/2.7.2/lib/ruby/2.7.0/rubygems.rb:1424 - cannot load such file -- rubygems/defaults/operating_system
Exception `LoadError' at /Users/dave/.asdf/installs/ruby/2.7.2/lib/ruby/2.7.0/rubygems.rb:1432 - cannot load such file -- rubygems/defaults/ruby
Exception `SyntaxError' at /Users/dave/.asdf/installs/ruby/2.7.2/lib/ruby/2.7.0/forwardable/impl.rb:5 - /Users/dave/.asdf/installs/ruby/2.7.2/lib/ruby/2.7.0/forwardable/impl.rb:5: syntax error, unexpected end-of-input
Exception `SyntaxError' at /Users/dave/.asdf/installs/ruby/2.7.2/lib/ruby/2.7.0/forwardable/impl.rb:5 - /Users/dave/.asdf/installs/ruby/2.7.2/lib/ruby/2.7.0/forwardable/impl.rb:5: syntax error, unexpected end-of-input
Exception `Errno::ENOENT' at /Users/dave/.asdf/installs/ruby/2.7.2/lib/ruby/2.7.0/csv.rb:641 - No such file or directory # rb_sysopen -
Exception `Errno::ENOENT' at test.rb:10 - No such file or directory # rb_sysopen -
/Users/dave/.asdf/installs/ruby/2.7.2/lib/ruby/2.7.0/csv.rb:641:in `initialize'
/Users/dave/.asdf/installs/ruby/2.7.2/lib/ruby/2.7.0/csv.rb:641:in `open'
/Users/dave/.asdf/installs/ruby/2.7.2/lib/ruby/2.7.0/csv.rb:641:in `open'
/Users/dave/.asdf/installs/ruby/2.7.2/lib/ruby/2.7.0/csv.rb:510:in `foreach'
test.rb:in `each'
Along with other noise.
If you really want to blow your mind, inside the rescue:
puts Thread.current.backtrace
Which outputs:
test.rb:21:in `backtrace'
test.rb:21:in `rescue in meth2'
test.rb:7:in `meth2'
test.rb:4:in `meth1'
test.rb:35:in `<main>'
I'm using eval on large blocks of code with this rescue block:
rescue => e
logger.error e.message
e.backtrace.each { |line| logger.error line }
end
The rescue block does not provide line number, etc. for the exception. I just get this:
undefined method `+' for nil:NilClass
which is not very helpful when there is a lot of code to evaluate. I tried various things such as:
eval(exp, binding, __FILE__, __LINE__)
but they do not provide any additional information on the line number of where the error is.
May be this can help. This answer is sort of based on details from this article. Assuming that you have expression to be evaluated in exp variable, I adjust the line number value passed as 3rd parameter to String#module_eval.
begin
exp = <<EOL
a = nil
a + 10
EOL
String.module_eval(exp,__FILE__, __LINE__ - 1 - exp.split("\n").size )
rescue Exception => e
puts e
puts e.backtrace
end
The output of above program:
undefined method `+' for nil:NilClass
E:/hello.rb:4:in `<main>'
E:/hello.rb:6:in `module_eval'
E:/hello.rb:6:in `<main>'
[Finished in 0.2s]
I have a method that should raise a custom error with a message. When I catch the error and raise my own custom error, it is still raising and printing the backtrace of the original error. I just want the custom error and message. Code below.
Method:
def load(configs)
begin
opts = {access_token: configs['token'],
api_endpoint: configs['endpoint'],
web_endpoint: configs['site'],
auto_paginate: configs['pagination']}
client = Octokit::Client.new(opts)
repos = client.org_repos(configs['org'])
repos.each do |r|
Project.create(name: r.name)
end
rescue Octokit::Unauthorized
raise GitConfigError, "boom"
end
#rescue Octokit::Unauthorized
end
class GitConfigError < StandardError
end
My test (which is failling):
context 'with incorrect git configs' do
before do
allow(loader).to receive(:load).and_raise Octokit::Unauthorized
end
it { expect{loader.load(configs)}.to raise_error(GitConfigError, "boom" ) }
end
Test Output:
GitProjectLoader#load with incorrect git configs should raise GitConfigError with "boom"
Failure/Error: it { expect{loader.load(configs)}.to raise_error(GitConfigError, "boom" ) }
expected GitConfigError with "boom", got #<Octokit::Unauthorized: Octokit::Unauthorized> with backtrace:
# ./spec/lib/git_project_loader_spec.rb:24:in `block (5 levels) in <top (required)>'
# ./spec/lib/git_project_loader_spec.rb:24:in `block (4 levels) in <top (required)>'
# ./spec/lib/git_project_loader_spec.rb:24:in `block (4 levels) in <top (required)>'
If you intend to test the handling of the Octokit::Unauthorized error, then raise the error anywhere before the rescue kicks in. Preferably, someplace where it would actually be raised.
Something like this, for example:
before do
allow(Octokit::Client).to receive(:new).and_raise(Octokit::Unauthorized)
end
And then:
expect{ loader.load(configs) }.to raise_error(GitConfigError, "boom" )
As a side note, I would discourage enclosing all lines of your method in a begin;rescue;end structure; you should enclose only the lines from which you are expecting errors.
You are not testing your code as you think. You have mocked it out.
The line
allow(loader).to receive(:load).and_raise Octokit::Unauthorized
replaces the load method on loader with a stub which just raises the named error.
Remove your before block, and it should test your code as intended. Note as written it will make a real request via Octokit, unless you mock that out instead.
class TwitterProfile < ActiveRecord::Base
def send_status_update(status_update)
if publish?
client = Twitter::Client.new( :oauth_token => authentication.token,
:oauth_token_secret => authentication.secret)
client.update(status_update.to_twitter_string)
end
rescue Exception => e
logger.info "Error publishing to twitter: #{e.to_s}"
end
end
There is a StatusUpdate model and an observer that reposts them to Twitter in after_create. I sometimes get the following exception:
NameError (undefined local variable or method `e' for #<TwitterProfile:0x00000004e44ab8>):
app/models/twitter_profile.rb:23:in `rescue in send_status_update'
app/models/twitter_profile.rb:18:in `send_status_update'
app/models/status_update_observer.rb:6:in `block in after_create'
app/models/status_update_observer.rb:4:in `after_create'
app/models/workout_observer.rb:5:in `after_update'
app/controllers/frames_controller.rb:76:in `update'
app/controllers/application_controller.rb:24:in `call'
app/controllers/application_controller.rb:24:in `block (2 levels) in <class:ApplicationController>'
app/controllers/application_controller.rb:10:in `block in <class:ApplicationController>'
What am I missing here?
I have one thing I know and one that's just a wild guess.
The thing I know is that you don't need to call to_s on an overall #{} expression; that will happen automatically. But it does no harm.
My wild guess is that your test case is not really running the code you have posted. What happens if you change e to f?
I should note that rescuing Exception itself is usually a bad idea. You should rescue RuntimeError or StandardError at the highest, and preferably something more specific. You can get fairly strange errors when rescuing Exception because you interfere with threads and interpreter-level events.
You're missing the 'begin' block of the begin/rescue clause.
Following is my method that might raise the exception.
Its a method of the CLI too that I am building.
Whenever the exception occurs, I want to catch that and just print my custom message on the terminal.
# variation 1
def self.validate(yaml_path)
begin
....
....
rescue
puts "Error"
end
end
# variation 2
def self.validate(yaml_path)
begin
....
....
rescue Exceptino => e
puts "Error: #{e.message}"
end
end
But the backtrace gets printed on the terminal.
How to avoid the backtrace to get printed?
± ../../bin/cf site create
ruby-1.8.7-p352
Error during processing: syntax error on line 52, col 10: ` - label: Price'
/Users/millisami/.rvm/rubies/ruby-1.8.7-p352/lib/ruby/1.8/yaml.rb:133:in `load': syntax error on line 52, col 10: ` - label: Price' (ArgumentError)
.... backtrace .....
.............
The answer was to rescue it on the executable file at bin/<exe>.
Thanks for suggesting
begin
Cf::CLI.start
rescue Psych::SyntaxError
$stderr.puts "\n\tError during processing: #{$!.message}\n\n"
end
The following code doesn't output the backtrace.
class CLS
def hi
begin
raise "X"
rescue
puts $!.message
end
end
end
CLS.new.hi
Have you checked to see if there is another point in the stack where another method is rescuing the exception, outputting the stack trace and then re-raising the exception?
The reason you're not rescuing the exception is because Psych::SyntaxError is not descended from StandardError, so a simple rescue won't catch it. You need to specify a descendant of Psych::SyntaxError:
>> require 'psych'
=> true
>> begin; raise Psych::SyntaxError; rescue; puts "GOT IT"; end
# Psych::SyntaxError: Psych::SyntaxError
# from (irb):8
# from /Users/donovan/.rvm/rubies/ruby-1.9.2-p180/bin/irb:16:in `<main>'
>> Psych::SyntaxError.ancestors
=> [Psych::SyntaxError, SyntaxError, ScriptError, Exception, Object, PP::ObjectMixin, Kernel, BasicObject]
>> begin; raise Psych::SyntaxError; rescue Exception; puts "GOT IT"; end
GOT IT
Notice that in my example rescue Exception does catch it. You should generally be as specific as you can when rescuing unless you really need to rescue all Exceptions. Be aware that suppressing backtraces is good when the exception is something you expect, but if you don't expect it in general it makes debugging much harder.