Ruby & Rails Error Handling

52
Error Handling in Ruby & Rails Simon Maynard - Bugsnag CTO @snmaynard

description

Some tips on handling errors in both Ruby and Rails

Transcript of Ruby & Rails Error Handling

Page 1: Ruby & Rails Error Handling

Error Handling in Ruby & RailsSimon Maynard - Bugsnag CTO

@snmaynard

Page 2: Ruby & Rails Error Handling

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

Page 3: Ruby & Rails Error Handling

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 #!?

Page 4: Ruby & Rails Error Handling

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

Page 5: Ruby & Rails Error Handling

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

Page 6: Ruby & Rails Error Handling

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

Page 7: Ruby & Rails Error Handling

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

Page 8: Ruby & Rails Error Handling

Raise Syntax

is the same as

raise MyError.new("Something Broke")

raise MyError, "Something Broke"

Page 9: Ruby & Rails Error Handling

Raise Syntax

is the same as

raise "Something Broke"

raise RuntimeError, "Something Broke"

Page 10: Ruby & Rails Error Handling

Raise Syntax

is the same as

raise

raise RuntimeError

Page 11: Ruby & Rails Error Handling

Raise Syntax

You can also pass a backtrace when raising an exception

def assert(value) raise(RuntimeError, "Something broke", caller) unless valueend

Page 12: Ruby & Rails Error Handling

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

Page 13: Ruby & Rails Error Handling

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

Page 14: Ruby & Rails Error Handling

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

Page 15: Ruby & Rails Error Handling

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

Page 16: Ruby & Rails Error Handling

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

Page 17: Ruby & Rails Error Handling

rescue Syntax

will rescue all MyError exceptions

beginrescue MyError => errorend

Page 18: Ruby & Rails Error Handling

rescue Syntax

is the same as

beginrescue => errorend

beginrescue StandardError => errorend

Page 19: Ruby & Rails Error Handling

Rescue Syntax

is the same as

beginrescueend

beginrescue StandardErrorend

Page 20: Ruby & Rails Error Handling

Rescue Syntax

You can also supply a list of classes to rescuebeginrescue MyError, IOError => errorend

Page 21: Ruby & Rails Error Handling

One Line Rescue Syntax

is the same asvalue = begin raiserescue "fallback_value"end

value = raise rescue "fallback_value"

Page 22: Ruby & Rails Error Handling

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

Page 23: Ruby & Rails Error Handling

Re-raising exception

is the same as

begin raiserescue raiseend

begin raiserescue raise $!end

Page 24: Ruby & Rails Error Handling

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

Page 25: Ruby & Rails Error Handling

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

Page 26: Ruby & Rails Error Handling

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

Page 27: Ruby & Rails Error Handling

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

Page 28: Ruby & Rails Error Handling

ALTERNATIVE Syntax

You can also use these commands without a begin section

def func(arg) raiserescue # Deal with exceptionensure # Always runend

Page 29: Ruby & Rails Error Handling

Ensure Syntax

Be careful with return inside an ensure!

def func(arg) raiseensure # This return swallows the exception return 5end

Page 30: Ruby & Rails Error Handling

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

Page 31: Ruby & Rails Error Handling

exception hierarchyException

NoMemoryError

ScriptErrorSignalExcepti

onStandardErro

rSystemExit fatal

LoadErrorSyntaxErro

r...

Interrupt ArgumentError

IOErrorIndexError

...

Page 32: Ruby & Rails Error Handling

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

Page 33: Ruby & Rails Error Handling

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...

Page 34: Ruby & Rails Error Handling

Raise is a method

• Raise is just a method on Kernel

• So we can override it!

Page 35: Ruby & Rails Error Handling

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

Page 36: Ruby & Rails Error Handling

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

Page 37: Ruby & Rails Error Handling

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

Page 38: Ruby & Rails Error Handling

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

Page 39: Ruby & Rails Error Handling

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)

Page 40: Ruby & Rails Error Handling

RACK Middleware

Middleware 1

Middleware 2

Middleware 3

Middleware 4

Rails App

Request Response

Page 41: Ruby & Rails Error Handling

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

Page 42: Ruby & Rails Error Handling

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

Page 43: Ruby & Rails Error Handling

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

Page 44: Ruby & Rails Error Handling

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

Page 45: Ruby & Rails Error Handling

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

Page 46: Ruby & Rails Error Handling

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

Page 47: Ruby & Rails Error Handling

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

Page 48: Ruby & Rails Error Handling

Hammertime

• https://github.com/avdi/hammertime

• Allows you to debug exception raises in real time in Ruby apps

Page 49: Ruby & Rails Error Handling

PRY RESCUE

• https://github.com/ConradIrwin/pry-rescue

• Allows you to debug uncaught exceptions in real time in Ruby apps

Page 50: Ruby & Rails Error Handling

Bugsnag

• http://bugsnag.com

• Tracks and groups your errors from development and production

• Get notified when someone on production sees a crash!

Page 51: Ruby & Rails Error Handling

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.

Page 52: Ruby & Rails Error Handling

Questions?Check out www.bugsnag.com