Ruby & Rails Error Handling
-
Upload
simon-maynard -
Category
Technology
-
view
16.626 -
download
2
description
Transcript of Ruby & Rails Error Handling
Error Handling in Ruby & RailsSimon Maynard - Bugsnag CTO
@snmaynard
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
What is AN ERROR?
An error occurs when code is unable to complete the task asked of it.
• Asked to do the impossible
• A mistake in the code
Account.find(nil)
account = Account.newacccount.name #!?
What is AN ERROR?
• An unexpected case
• Failure of an external element
case account.typewhen "free" then #when "paid" then #else # Unexpected!end
# Database down!account.save
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
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
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
Raise Syntax
is the same as
raise MyError.new("Something Broke")
raise MyError, "Something Broke"
Raise Syntax
is the same as
raise "Something Broke"
raise RuntimeError, "Something Broke"
Raise Syntax
is the same as
raise
raise RuntimeError
Raise Syntax
You can also pass a backtrace when raising an exception
def assert(value) raise(RuntimeError, "Something broke", caller) unless valueend
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
How does raise build the exception?
You might think that raise does this
But actually it does this...
def raise(klass, msg, trace) exception = klass.new(message) # ...end
def raise(klass, msg, trace) exception = klass.exception(message) # ...end
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
How does raise build the exception?
This means we can implement our own exception methods
then we can throw an instance of own object
class Account def exception(message="Bad Account!") ValidationError.new("#{message}: #{self.errors.inspect}") endend
raise account unless account.save
Global Error Object$! contains a reference to the exception currently being raised
You can also require “english” to use the slightly more readable $ERROR_INFO
begin raiserescue puts $!.inspectend
require "english"begin raiserescue puts $ERROR_INFO.inspectend
rescue Syntax
will rescue all MyError exceptions
beginrescue MyError => errorend
rescue Syntax
is the same as
beginrescue => errorend
beginrescue StandardError => errorend
Rescue Syntax
is the same as
beginrescueend
beginrescue StandardErrorend
Rescue Syntax
You can also supply a list of classes to rescuebeginrescue MyError, IOError => errorend
One Line Rescue Syntax
is the same asvalue = begin raiserescue "fallback_value"end
value = raise rescue "fallback_value"
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 modend
begin raise "sample message"rescue match_message(/sample/) # Ignoreend
Re-raising exception
is the same as
begin raiserescue raiseend
begin raiserescue raise $!end
Raising in rescue
You can also change the exception message before re-raising
begin raiserescue => err # Re raise with different message raise err, “Different message”end
Raising in rescue
You can raise in a rescue
You lose the context of the real error!
Don’t do this!
def func raiserescue raise "totally new exception"end
Raising in rescueInstead 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 endend
def func raiserescue raise MyError.new("Something broke")end
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.5ensure # Always runend
ALTERNATIVE Syntax
You can also use these commands without a begin section
def func(arg) raiserescue # Deal with exceptionensure # Always runend
Ensure Syntax
Be careful with return inside an ensure!
def func(arg) raiseensure # This return swallows the exception return 5end
RETRY
You can also easily retry using the retry keyword
def func(arg) attempts = 0 begin attempts += 1 raise rescue retry if attempts < 3 endend
exception hierarchyException
NoMemoryError
ScriptErrorSignalExcepti
onStandardErro
rSystemExit fatal
LoadErrorSyntaxErro
r...
Interrupt ArgumentError
IOErrorIndexError
...
Exception hierarchy
For example,
This program is almost unkillable! Don’t catch Exception!
while true do begin line = STDIN.gets # heavy processing rescue Exception => e puts "caught exception #{e}! ohnoes!" endend
Exception Hierarchy
You can even prevent an exit call,
You can’t catch an exit!(1) however...
begin exit(1) # Or abort()rescue Exception puts "Guess again!"end# Continue...
Raise is a method
• Raise is just a method on Kernel
• So we can override it!
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 endendclass Object include RaiseDebugend
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 endend
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.0catch (:done) do 1.upto(INFINITY) do |i| 1.upto(INFINITY) do |j| if some_condition throw :done end end endend
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
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 requestconfig.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)
RACK Middleware
Middleware 1
Middleware 2
Middleware 3
Middleware 4
Rails App
Request Response
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 endend
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 = append
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
Rendering a 500 page
• Rails uses this to handle errors, for example in ShowExceptions middleware
• Rails rescues the exception here, and renders a nice 500 error page
def call(env) @app.call(env)rescue Exception => exception raise exception if env['action_dispatch.show_exceptions'] == false render_exception(env, exception)end
Bugsnag Logging middleware
• Here is a simplified version of the Bugsnag error logging middleware
• But you need to make sure this goes in you middleware stack JUST before you render the 500 page!
def call(env) @app.call(env)rescue Exception => exception Bugsnag.notify(exception) raiseend
SHOW The middleware stack
• Rails has given you an awesome tool to show your middleware stack $ rake middleware
...use ActionDispatch::ShowExceptionsuse ActionDispatch::DebugExceptionsuse Bugsnag::Rack...use ActionDispatch::Cookiesuse ActionDispatch::Session::CookieStoreuse ActionDispatch::Flashuse ActionDispatch::ParamsParser...run YourApp::Application.routes
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
Hammertime
• https://github.com/avdi/hammertime
• Allows you to debug exception raises in real time in Ruby apps
PRY RESCUE
• https://github.com/ConradIrwin/pry-rescue
• Allows you to debug uncaught exceptions in real time in Ruby apps
Bugsnag
• http://bugsnag.com
• Tracks and groups your errors from development and production
• Get notified when someone on production sees a crash!
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.
Questions?Check out www.bugsnag.com