Ruby Logo

Custom-exceptions

Learn about custom-exceptions in Ruby programming.

Home Ruby Custom-exceptions

Ruby Custom Exceptions Mastery

Learn how to design meaningful, domain-specific exceptions that improve error handling, simplify debugging, and make your Ruby applications more expressive and maintainable.

Why Create Custom Exceptions?

Custom Exceptions: Domain-specific error classes that provide meaningful context about what went wrong in your application. They enable precise handling, better debugging, and self-documenting code.

Benefits

  • Semantic meaning – clear indication of what failed
  • Selective handling – rescue specific error types
  • Rich context – include attributes and helpers
  • Better debugging – informative messages and data

Problems They Solve

  • Generic errors lack context
  • Broad rescue hides root causes
  • Inflexible handling for different failure modes

Basic Custom Exception Classes

# Custom exceptions typically inherit from StandardError
class PaymentError < StandardError; end
class UserNotFoundError < StandardError; end
class ValidationError < StandardError; end
def find_user(id) raise UserNotFoundError, "User with ID #{id} not found" unless user_exists?(id) end

Rich Exceptions with Data & Helpers

class ValidationError < StandardError attr_reader :field, :value, :errors def initialize(message, field: nil, value: nil) super(message) @field = field @value = value @errors = {} end def add_error(field, message) @errors[field] ||= [] @errors[field] << message end def full_messages msgs = [message] @errors.each { |k, arr| arr.each { |m| msgs << "#{k}: #{m}" } } msgs end end

Try It Yourself - Interactive Practice

Edit and run this code to experiment with custom exceptions.

Interactive Code Runner

Ruby Code Editor
Output will appear here when you run the code...

Best Practices & Common Pitfalls

✅ Best Practices

  • Inherit from StandardError (not Exception)
  • Prefer descriptive names and helpful messages
  • Attach context data via attributes and helpers
  • Design small, logical hierarchies for flexible rescue

❌ Common Pitfalls

  • Using generic errors everywhere
  • Overly deep or broad exception hierarchies
  • Swallowing exceptions without logging/handling

Custom Exceptions Checklist

Design

  • Pick clear names and messages
  • Attach relevant context

Handling

  • Rescue narrowly when possible
  • Log and surface helpful details

Ruby Custom Exceptions Mastery

Master the art of creating custom exception classes in Ruby. Learn how to design meaningful error types that make your applications more maintainable, debuggable, and expressive.

Why Create Custom Exceptions?

Custom Exceptions: Domain-specific error classes that provide meaningful context about what went wrong in your application. They make error handling more precise, debugging easier, and code more self-documenting.

Benefits Why Use Custom Exceptions?

  • Semantic meaning: Clear indication of what failed
  • Selective handling: Rescue specific error types
  • Rich context: Include relevant data with errors
  • Better debugging: More informative error messages
  • API design: Clear error contracts for users
  • Maintainability: Easier to modify error handling

Problems Problems Custom Exceptions Solve

  • Generic errors: "Something went wrong" tells us nothing
  • Lost context: Important error details are missing
  • Broad rescue: Catching too many unrelated errors
  • Poor UX: Users see cryptic technical messages
  • Hard debugging: Difficult to trace error sources
  • Inflexible handling: Same response to different errors

Real-World Analogy

Think of custom exceptions like different types of alarms: A fire alarm sounds different from a security alarm or a medical emergency alert. Each has a specific sound and response protocol. Similarly, custom exceptions help your code "sound different alarms" for different types of problems, allowing appropriate responses to each situation.

Basic Custom Exception Classes

Basic Pattern: Custom exceptions inherit from StandardError (or its subclasses) and can include additional data and methods to provide context about what went wrong.

Simple Custom Exceptions

# Simplest custom exception - just inherits from StandardError
class
CustomError
<
StandardError
end

# Domain-specific exceptions
class
PaymentError
<
StandardError
end

class
UserNotFoundError
<
StandardError
end

class
ValidationError
<
StandardError
end

# Usage
def
process_payment
(amount)
raise
PaymentError
,
"Insufficient funds"
if
amount > balance
raise
PaymentError
,
"Invalid amount"
if
amount <=
0
# Process payment...
end

def
find_user
(id)
raise
UserNotFoundError
,
"User with ID #{id} not found"
unless
user_exists?(id)
# Return user...
end

Exceptions with Custom Messages

# Exception with default message
class
DatabaseConnectionError
<
StandardError
def
initialize
(message =
"Unable to connect to database"
)
super
(message)
end
end

# Exception with formatted message
class
FileNotFoundError
<
StandardError
def
initialize
(filename)
super
(
"File not found: #{filename}"
)
@filename = filename
end

attr_reader :filename
end

# Usage examples
begin
raise
DatabaseConnectionError
# Uses default message
rescue
DatabaseConnectionError
=> e
puts e.message
# "Unable to connect to database"
end

begin
raise
FileNotFoundError
.new(
"config.yml"
)
rescue
FileNotFoundError
=> e
puts e.message
# "File not found: config.yml"
puts e.filename
# "config.yml"
end

Rich Exception Classes with Data

Rich Exceptions: Include additional data, status codes, and methods to provide comprehensive context about errors. These make debugging and error handling much more powerful.

Validation Errors with Field Details

# Comprehensive validation error class
class
ValidationError
<
StandardError
attr_reader :field, :value, :rule, :errors

def
initialize
(message, field:
nil
, value:
nil
, rule:
nil
, errors: {})
super
(message)
@field = field
@value = value
@rule = rule
@errors = errors
end

def
add_error
(field, message)
@errors[field] ||= []
@errors[field] << message
end

def
has_errors?
!@errors.empty?
end

def
full_messages
messages = [message]
@errors.each do |field, field_errors|
field_errors.each { |error| messages <<
"#{field}: #{error}"
}
end
messages
end

def
to_hash
{
message: message,
field: @field,
value: @value,
rule: @rule,
errors: @errors
}
end
end

# Usage example
def
validate_user
(user_data)
error =
ValidationError
.new(
"User validation failed"
)

error.add_error(:email,
"is required"
)
if
user_data[:email].nil?
error.add_error(:email,
"must be valid"
)
unless
valid_email?(user_data[:email])
error.add_error(:age,
"must be 18 or older"
)
if
user_data[:age] <
18

raise
error
if
error.has_errors?
end

HTTP-Style Errors with Status Codes

# HTTP-style error with status codes
class
APIError
<
StandardError
attr_reader :status_code, :error_code, :details, :timestamp

def
initialize
(message, status_code:
500
, error_code:
nil
, details: {})
super
(message)
@status_code = status_code
@error_code = error_code
@details = details
@timestamp =
Time
.now
end

def
client_error?
@status_code >=
400
&& @status_code <
500
end

def
server_error?
@status_code >=
500
end

def
to_json
{
error: {
message: message,
status_code: @status_code,
error_code: @error_code,
details: @details,
timestamp: @timestamp.iso8601
}
}.to_json
end
end

# Specific API error types
class
NotFoundError
<
APIError
def
initialize
(resource, id =
nil
)
message = id ?
"#{resource} with ID #{id} not found"
:
"#{resource} not found"
super
(message, status_code:
404
, error_code:
"NOT_FOUND"
, details: { resource: resource, id: id })
end
end

class
UnauthorizedError
<
APIError
def
initialize
(action =
nil
)
message = action ?
"Unauthorized to #{action}"
:
"Unauthorized access"
super
(message, status_code:
401
, error_code:
"UNAUTHORIZED"
, details: { action: action })
end
end

Business Logic Errors with Context

# Business rule violation with rich context
class
BusinessRuleError
<
StandardError
attr_reader :rule_name, :entity, :context, :suggestions

def
initialize
(message, rule_name:, entity:
nil
, context: {}, suggestions: [])
super
(message)
@rule_name = rule_name
@entity = entity
@context = context
@suggestions = suggestions
end

def
detailed_message
msg = [
"Business Rule Violation: #{@rule_name}"
]
msg <<
"Error: #{message}"
msg <<
"Entity: #{@entity.class.name} (ID: #{@entity.id})"
if
@entity&.respond_to?(:id)

unless
@context.empty?
msg <<
"Context:"
@context.each { |key, value| msg <<
" #{key}: #{value}"
}
end

unless
@suggestions.empty?
msg <<
"Suggestions:"
@suggestions.each { |suggestion| msg <<
" - #{suggestion}"
}
end

msg.join(
"\n"
)
end
end

# Usage in business logic
def
transfer_money
(from_account, to_account, amount)
if
from_account.balance < amount
raise
BusinessRuleError
.new(
"Insufficient funds for transfer"
,
rule_name:
"sufficient_balance"
,
entity: from_account,
context: {
requested_amount: amount,
available_balance: from_account.balance,
shortfall: amount - from_account.balance
},
suggestions: [
"Reduce transfer amount to $#{from_account.balance}"
,
"Add funds to account first"
,
"Contact customer service for overdraft options"
]
)
end

# Proceed with transfer...
end

Building Exception Hierarchies

Exception Hierarchies: Organize related exceptions in inheritance trees. This allows you to catch broad categories of errors or specific types, making your error handling both flexible and precise.

Application-Level Exception Hierarchy

# Base application error
module
MyApp
class
Error
<
StandardError
attr_reader :context, :severity

def
initialize
(message, context: {}, severity:
:error
)
super
(message)
@context = context
@severity = severity
end

def
critical?
@severity ==
:critical
end
end

# Authentication & Authorization errors
class
SecurityError
<
Error
;
end

class
AuthenticationError
<
SecurityError
def
initialize
(message =
"Authentication failed"
, **options)
super
(message, **options)
end
end

class
AuthorizationError
<
SecurityError
def
initialize
(action, **options)
super
(
"Not authorized to #{action}"
, **options)
@action = action
end

attr_reader :action
end

# Data-related errors
class
DataError
<
Error
;
end

class
ValidationError
<
DataError
;
end
class
NotFoundError
<
DataError
;
end
class
DuplicateError
<
DataError
;
end

# External service errors
class
ExternalServiceError
<
Error
def
initialize
(service_name, **options)
super
(
"External service error: #{service_name}"
, **options)
@service_name = service_name
end

attr_reader :service_name
end

class
APIError
<
ExternalServiceError
;
end
class
DatabaseError
<
ExternalServiceError
;
end
class
NetworkError
<
ExternalServiceError
;
end
end

Using the Exception Hierarchy

# Using the hierarchy for flexible error handling
def
process_user_request
(user, action, data)
begin
authenticate_user(user)
authorize_action(user, action)
validate_data(data)
save_to_database(data)
send_notification(user)

# Catch specific errors for tailored handling
rescue
MyApp::AuthenticationError
=> e
log_security_event(e)
{ error:
"Please log in"
, status:
401
}

rescue
MyApp::AuthorizationError
=> e
log_security_event(e)
{ error:
"Access denied"
, status:
403
}

rescue
MyApp::ValidationError
=> e
{ error:
"Invalid data"
, details: e.context, status:
400
}

# Catch broad categories
rescue
MyApp::SecurityError
=> e
log_security_event(e)
{ error:
"Security error"
, status:
403
}

rescue
MyApp::DataError
=> e
{ error:
"Data error"
, message: e.message, status:
400
}

rescue
MyApp::ExternalServiceError
=> e
log_service_error(e)
notify_ops_team(e)
if
e.critical?
{ error:
"Service temporarily unavailable"
, status:
503
}

# Catch all application errors
rescue
MyApp::Error
=> e
log_application_error(e)
{ error:
"Application error"
, status:
500
}

# Unexpected errors
rescue
=> e
log_unexpected_error(e)
notify_developers(e)
{ error:
"Internal server error"
, status:
500
}
end
end

Interactive Practice: Custom Exceptions

Practice Time: Experiment with creating and using custom exceptions. Try building your own exception hierarchy and see how it makes error handling more expressive and maintainable.

Interactive Code Runner

Ruby Code Editor
Output will appear here when you run the code...

Best Practices & Guidelines

✅ Best Practices

  • Inherit from StandardError: Never inherit from Exception directly
  • Use meaningful names: Exception names should clearly indicate what failed
  • Include context data: Store relevant information as attributes
  • Provide good messages: Make error messages helpful for users and developers
  • Build hierarchies: Group related exceptions for flexible catching
  • Document exceptions: Specify what exceptions methods can raise
  • Use modules for namespacing: Avoid global exception name conflicts

❌ Common Pitfalls

  • Too many exceptions: Don't create exceptions for every possible error
  • Generic names: Avoid names like Error, Failure, or Problem
  • No context: Don't just inherit without adding value
  • Deep hierarchies: Keep inheritance trees shallow and logical
  • Exposing internals: Don't leak implementation details in error messages
  • Poor inheritance: Don't inherit from built-in exceptions inappropriately
  • Missing documentation: Always document custom exceptions

Design Guidelines

Naming Conventions:
  • End with "Error" for clarity
  • Use domain-specific prefixes
  • Be descriptive but concise
  • Reflect the nature of the problem
Content Guidelines:
  • Include relevant data as attributes
  • Provide helpful error messages
  • Add methods for common queries
  • Consider serialization needs

Custom Exceptions Mastery Summary

You've Mastered Ruby Custom Exceptions!

Domain-Specific Errors

Meaningful exceptions that reflect your business logic

Rich Context

Exceptions with data, methods, and debugging information

Organized Hierarchies

Logical inheritance trees for flexible error handling

Custom exceptions are a powerful tool for creating maintainable, debuggable applications. They provide semantic meaning to errors, enable precise error handling, and make your code self-documenting. Use them to create clear contracts between different parts of your application and to provide excellent user experiences.

Rescue Modifiers & Flow Control
Retry Patterns & Error Recovery

Quick Navigation

Read Topic
Watch Video Tutorial

Related Topics

Variables & Constants → Arrays → Methods →

Back to Ruby Home

Video Tutorial

Watch and learn custom-exceptions

Pro Tip: After reading through the content above, watch this video to reinforce your understanding and see the concepts in action!