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