Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Living Without Exceptions

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

Living Without Exceptions

Avatar for Radoslav Stankov

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