Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Living Without Exceptions

Living Without Exceptions

Radoslav Stankov

June 13, 2024
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

  1. !

  2. % Fix bugs & Fix exceptions ' Bump dependancies (

    Pay technical dept ) Catchup on projects " Happy Friday
  3. % Fix bugs & Fix exceptions ' Bump dependancies (

    Pay technical dept ) Catchup on projects " Happy Friday
  4. % Fix bugs & Fix exceptions ' Bump dependancies (

    Pay technical dept ) Catchup on projects " Happy Friday
  5. “Be explicit around the exceptions. Handle specific errors and have

    explanations of why they happen.” * Rado's tip
  6. “Have a Slack channel where every new exception is being

    logged. Log deployments in this channel as well, so you can correlate.” * Rado's tip
  7. – Brian Kernighan and Rob Pike, The Practice of Programming

    “Exceptions shouldn't be expected” Use exceptions only for exceptional situations. […] Exceptions are often overused. Because they distort the flow of control, they can lead to convoluted constructions that are prone to bugs. It is hardly exceptional to fail to open a file; generating an exception in this case strikes us as over-engineering.
  8. Sentry.configure do |config| # Note(rstankov): Exclude unactionable errors config.excluded_exceptions =

    [ 'Rack::Timeout::RequestExpiryError', 'Rack::Timeout::RequestTimeoutException', 'ActionController::RoutingError', 'ActionController::InvalidAuthenticityToken', 'ActionDispatch::ParamsParser::ParseError', 'Sidekiq::Shutdown', ] end
  9. # NOTE(rstankov): Fix invalid byte sequence in UTF-8. More info:

    # - https://robots.thoughtbot.com/fight-back-utf-8-invalid-byte-sequences module Handle::InvalidByteSequence extend self def call(string) return if string.nil? string.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '' ) end end
  10. “I put most of the code related to exceptions in

    a single module named Handle” * Rado's tip
  11. ✅ Check for other accounts without a subscription ✅ Find

    out why those accounts don't have a subscription , Steps to fix -
  12. ✅ Check for other accounts without a subscription ✅ Find

    out why those accounts don't have a subscription , Steps to fix -
  13. ✅ Check for other accounts without a subscription ✅ Find

    out why those accounts don't have a subscription ✅ Fix this / , Steps to fix -
  14. ✅ Check for other accounts without a subscription ✅ Find

    out why those accounts don't have a subscription ✅ Fix this / , Steps to fix -
  15. ✅ Check for other accounts without a subscription ✅ Find

    out why those accounts don't have a subscription ✅ Fix this / ✅ Add missing subscriptions to accounts , Steps to fix -
  16. “I have a module named ErrorReporting, which helps me capture

    errors that won't get to the user and provides a centralized place for error tracking.” * Rado's tip
  17. class Frontend::GraphqlController < Frontend::BaseController before_action :authenticate def index render json:

    Graph::Schema.execute(query, variables: variables, context: context) rescue => e handle_error e end private def authenticate # ... 
 ErrorReporting.assign_user(@user) end
 def handle_error(error) if Rails.env.development? logger.error error.message logger.error error.backtrace.join("\n") render json: { error: { message: error.message, backtrace: error.backtrace } }, status: 500 elsif Rails.env.test? p error.message p error.backtrace render json: { error: { message: error.message, backtrace: error.backtrace } }, status: 500 else ErrorReporting.capture(e, query: query) render json: { error: { message: 'SERVER_ERROR' }, data: {} }, status: 500 end end end
  18. class Frontend::GraphqlController < Frontend::BaseController before_action :authenticate def index render json:

    Graph::Schema.execute(query, variables: variables, context: context) rescue => e handle_error e end private def authenticate # ... 
 ErrorReporting.assign_user(@user) end
 def handle_error(error) if Rails.env.development? logger.error error.message logger.error error.backtrace.join("\n") render json: { error: { message: error.message, backtrace: error.backtrace } }, status: 500 elsif Rails.env.test? p error.message p error.backtrace render json: { error: { message: error.message, backtrace: error.backtrace } }, status: 500 else ErrorReporting.capture(e, query: query) render json: { error: { message: 'SERVER_ERROR' }, data: {} }, status: 500 end end end
  19. class Frontend::GraphqlController < Frontend::BaseController before_action :authenticate def index render json:

    Graph::Schema.execute(query, variables: variables, context: context) rescue => e handle_error e end private def authenticate # ... 
 ErrorReporting.assign_user(@user) end
 def handle_error(error) if Rails.env.development? logger.error error.message logger.error error.backtrace.join("\n") render json: { error: { message: error.message, backtrace: error.backtrace } }, status: 500 elsif Rails.env.test? p error.message p error.backtrace render json: { error: { message: error.message, backtrace: error.backtrace } }, status: 500 else ErrorReporting.capture(e, query: query) render json: { error: { message: 'SERVER_ERROR' }, data: {} }, status: 500 end end end
  20. class Frontend::GraphqlController < Frontend::BaseController before_action :authenticate def index render json:

    Graph::Schema.execute(query, variables: variables, context: context) rescue => e handle_error e end private def authenticate # ... 
 ErrorReporting.assign_user(@user) end
 def handle_error(error) if Rails.env.development? logger.error error.message logger.error error.backtrace.join("\n") render json: { error: { message: error.message, backtrace: error.backtrace } }, status: 500 elsif Rails.env.test? p error.message p error.backtrace render json: { error: { message: error.message, backtrace: error.backtrace } }, status: 500 else ErrorReporting.capture(e, query: query) render json: { error: { message: 'SERVER_ERROR' }, data: {} }, status: 500 end end end
  21. “Invest in your monitoring and exception traceability. When you have

    a hard time racing an exception. Ask yourself - what more information I need? 0 Then add it.” * Tip
  22. module Handle::RaceCondition extend self UNIQUE_ACTIVE_RECORD_ERROR = 'has already been taken'.freeze

    def call retries ||= 2 yield rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation retries -= 1 raise unless retries.nonzero? retry rescue ActiveRecord::RecordInvalid => e raise unless e.message.include? UNIQUE_ACTIVE_RECORD_ERROR retries -= 1 raise unless retries.nonzero? retry end end
  23. module Handle::NetworkErrors extend self ERRORS = [ EOFError, Errno::ECONNRESET, Errno::EINVAL,

    Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, Timeout::Error, # ... ] def ===(error) ERRORS.any? { |error_class| error_class === error } end end
  24. 2

  25. module Handle::NetworkErrors extend self ERRORS = [ # ... Faraday::ConnectionFailed,

    Faraday::TimeoutError, RestClient::BadGateway, RestClient::BadRequest, # ... ] def ===(error) ERRORS.any? { |error_class| error_class === error } end end
  26. class Notifications::ScheduleJob < ApplicationJob queue_as :notifications def perform(kind:, object:, subscriber_id:)

    Notifications::Schedule.call( kind: kind, object: object, subscriber_id: subscriber_id, ) end end
  27. class Notifications::ScheduleJob < ApplicationJob include Handle::Job::DeserializationError queue_as :notifications def perform(kind:,

    object:, subscriber_id:) Notifications::Schedule.call( kind: kind, object: object, subscriber_id: subscriber_id, ) end end
  28. * Be explicit around the exceptions * Reduce noise *

    Don't hide exceptions * Invest in your monitoring * Have tooling around handling exceptions 3 Recap