Ruby & Rails Error Handling

Post on 20-May-2015

16.626 views 2 download

Tags:

description

Some tips on handling errors in both Ruby and Rails

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