Cloud Frontiers: A Deep Dive into Serverless Spatial Data and FME
Ruby & Rails Error Handling
1. Error Handling in Ruby & Rails
Simon Maynard - Bugsnag CTO
@snmaynard
2. What is Bugsnag?
• We help developers log and track their errors
• Ruby was the first language we supported
• Now support Python, PHP, Javascript, Node.js, iOS, Android & more!
• Processing millions of errors every day
3. What is AN ERROR?
An error occurs when code is unable to complete the task asked of it.
• Asked to do the impossible
Account.find(nil)
• A mistake in the code
account = Account.new
acccount.name #!?
4. What is AN ERROR?
• An unexpected case
case account.type
when "free" then #
when "paid" then #
else
# Unexpected!
end
• Failure of an external element
# Database down!
account.save
5. How to report an error?
• Raise an exception
• Only when the error is truly exceptional
• If the error is unexpected, or a sign of bad code
• Raising should be unexpected, it’s slow
6. How to report an error?
• Return an error value
• If its to be expected or part of "normal" operation
def find(id)
raise InvalidIdError, "#{id} id an invalid id" unless validate_id(id)
return data[id]
end
7. Raise Or Fail
• You can use raise to raise an exception.
• You can also use fail to raise an exception.
def func
fail "Not implemented"
rescue => e
raise unless e.message == "Not implemented"
end
8. Raise Syntax
raise MyError.new("Something Broke")
is the same as
raise MyError, "Something Broke"
9. Raise Syntax
raise "Something Broke"
is the same as
raise RuntimeError, "Something Broke"
11. Raise Syntax
You can also pass a backtrace when raising an exception
def assert(value)
raise(RuntimeError, "Something broke", caller) unless value
end
12. What does raise actually do?
• Raise does four things,
• Builds an exception object
• Sets the backtrace
• Sets the global error object ($!)
• Starts unwinding the stack
13. How does raise build the exception?
You might think that raise does this
def raise(klass, msg, trace)
exception = klass.new(message)
# ...
end
But actually it does this...
def raise(klass, msg, trace)
exception = klass.exception(message)
# ...
end
14. How does raise build the exception?
• Exception.exception
• The same as Exception.new()
• Exception#exception
• With no arguments, it returns self
• With a message, it returns a new exception with the message set
15. How does raise build the exception?
This means we can implement our own exception methods
class Account
def exception(message="Bad Account!")
ValidationError.new("#{message}: #{self.errors.inspect}")
end
end
then we can throw an instance of own object
raise account unless account.save
16. Global Error Object
$! contains a reference to the exception currently being raised
begin
raise
rescue
puts $!.inspect
end
You can also require “english” to use the slightly more readable
$ERROR_INFO
require "english"
begin
raise
rescue
puts $ERROR_INFO.inspect
end
17. rescue Syntax
begin
rescue MyError => error
end
will rescue all MyError exceptions
18. rescue Syntax
begin
rescue => error
end
is the same as
begin
rescue StandardError => error
end
19. Rescue Syntax
begin
rescue
end
is the same as
begin
rescue StandardError
end
20. Rescue Syntax
You can also supply a list of classes to rescue
begin
rescue MyError, IOError => error
end
21. One Line Rescue Syntax
value = raise rescue "fallback_value"
is the same as
value = begin
raise
rescue
"fallback_value"
end
22. Dynamic rescues
def match_message(regex)
mod = Module.new
(class << mod; self; end).instance_eval do
define_method(:===) do |e|
regex === e.message
end
end
mod
end
begin
raise "sample message"
rescue match_message(/sample/)
# Ignore
end
24. Raising in rescue
You can also change the exception message before re-raising
begin
raise
rescue => err
# Re raise with different message
raise err, “Different message”
end
25. Raising in rescue
You can raise in a rescue
def func
raise
rescue
raise "totally new exception"
end
You lose the context of the real error!
Don’t do this!
26. Raising in rescue
Instead you can keep a reference to the original exception
class MyError < StandardError
attr_accessor :original_exception
def initialize(msg, original_exception=$!)
super(msg)
self.original_exception = original_exception
end
end
def func
raise
rescue
raise MyError.new("Something broke")
end
27. Ensure Syntax
Ensure allows you to ensure that code is run, regardless of whether an exception
is raised or not.
begin
raise unless rand < 0.5
ensure
# Always run
end
28. ALTERNATIVE Syntax
You can also use these commands without a begin section
def func(arg)
raise
rescue
# Deal with exception
ensure
# Always run
end
29. Ensure Syntax
Be careful with return inside an ensure!
def func(arg)
raise
ensure
# This return swallows the exception
return 5
end
30. RETRY
You can also easily retry using the retry keyword
def func(arg)
attempts = 0
begin
attempts += 1
raise
rescue
retry if attempts < 3
end
end
32. Exception hierarchy
For example,
while true do
begin
line = STDIN.gets
# heavy processing
rescue Exception => e
puts "caught exception #{e}! ohnoes!"
end
end
This program is almost unkillable! Don’t catch Exception!
33. Exception Hierarchy
You can even prevent an exit call,
begin
exit(1) # Or abort()
rescue Exception
puts "Guess again!"
end
# Continue...
You can’t catch an exit!(1) however...
34. Raise is a method
• Raise is just a method on Kernel
• So we can override it!
35. Raise is a method
We can add debugging information to each raise
module RaiseDebug
def raise(*args)
super *args
rescue Exception
puts "Raising exception: #{$!.inspect}"
super *args
end
end
class Object
include RaiseDebug
end
36. Uncaught Errors
We can use a combination of $! and the ruby exit handler to log uncaught errors
at_exit do
if $!
open('error.log', 'a') do |log_file|
error = {
timestamp: Time.now.utc,
message: $!.message,
trace: $!.backtrace,
}
log_file.write(error.to_json)
end
end
end
37. Throw/Catch
Ruby can also throw, but its not for errors.
Use throw to unwrap the stack in a non-exceptional case, saves you from using
multiple break commands
INFINITY = 1.0 / 0.0
catch (:done) do
1.upto(INFINITY) do |i|
1.upto(INFINITY) do |j|
if some_condition
throw :done
end
end
end
end
38. How does rails deal with exceptions?
When there is an error in your rails app, ideally we want these things to happen
• 500 page rendered to show the user something went wrong
• Error logged with enough information so we can fix it
• Rails to continue serving requests
39. How does rails deal with exceptions?
• Rails uses a Rack app to process every request.
• Rack apps have a middleware stack
• You can easily add your own middleware so you can execute code on every
request
config.middleware.use(new_middleware, args)
config.middleware.insert_before(existing_middleware, new_middleware, args)
config.middleware.insert_after(existing_middleware, new_middleware, args)
config.middleware.delete(middleware)
41. Example RACK Middleware
Here is an example of a no-op middleware
module OurMiddleware
class Rack
def initialize(app)
@app = app
end
def call(env)
@app.call(env)
end
end
end
42. Example RACK Middleware
• Initialize called when rails app starts
• Takes a single parameter, which is the next middleware in the stack
• Perform any other initialization for your middleware
def initialize(app)
@app = app
end
43. Example RACK Middleware
• Call is called for every request
• @app.call calls the next middleware in the stack (or your app itself)
def call(env)
response = @app.call(env)
end
44. Rendering a 500 page
• Rails uses this to handle errors, for example in ShowExceptions middleware
def call(env)
@app.call(env)
rescue Exception => exception
raise exception if env['action_dispatch.show_exceptions'] == false
render_exception(env, exception)
end
• Rails rescues the exception here, and renders a nice 500 error page
45. Bugsnag Logging middleware
• Here is a simplified version of the Bugsnag error logging middleware
def call(env)
@app.call(env)
rescue Exception => exception
Bugsnag.notify(exception)
raise
end
• But you need to make sure this goes in you middleware stack JUST before you
render the 500 page!
46. SHOW The middleware stack
• Rails has given you an awesome tool to show your middleware stack
$ rake middleware
...
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use Bugsnag::Rack
...
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
...
run YourApp::Application.routes
47. Better Errors
• https://github.com/charliesome/better_errors
• Better version of DebugExceptions, used in development on Rails
• Allows you to debug crashes when they happen
48. Hammertime
• https://github.com/avdi/hammertime
• Allows you to debug exception raises in real time in Ruby apps
49. PRY RESCUE
• https://github.com/ConradIrwin/pry-rescue
• Allows you to debug uncaught exceptions in real time in Ruby apps
50. Bugsnag
• http://bugsnag.com
• Tracks and groups your errors from development and production
• Get notified when someone on production sees a crash!
51. Find out more
• Avdi Grimm has a great book on Ruby failure handling - I highly recommend it
(http://exceptionalruby.com/)
• When looking into rails error handling, delving into Rails source is recommended.
Return an error value if its common or people will use to test Validation function shouldnt raise if invalid e.g mongoid find raising by default, at least you can change if you want
raise = fail raise common some use fail to be when failing for first time, raise in rescue blocks
exception is like to_s, but for exceptions
Can attach exception methods to objects easily, so they can be raised
These are just examples, empty rescue blocks are a code smell, at least have a log in there!
Bad as it catches all StandardErrors, you should catch explicit exception types, more on that later
Rescue calls === on the error and the object passed to rescue, so we can override
This is a common pattern in ruby, getting more popular - rails does it when you have an error in your view for example converts to template error
Exception hierarchy shows why you should never catch exception
Be specific when you catch something
Note here I rescue exception, but I always re-raise :-)
Sinatra uses this when using etags i believe, to prevent recalculating a still valid response
Middleware performance is important Crashes in middleware are very bad