Overview
Ruby provides two primary mechanisms for raising exceptions: the raise
method and its alias fail
. Both methods create and throw exceptions, enabling developers to signal error conditions and interrupt normal program execution flow. Ruby implements exception handling through a class hierarchy rooted in the Exception
class, with StandardError
serving as the base class for most application-level exceptions.
The raise
method accepts various argument patterns, including exception classes, exception instances, and string messages. When called without arguments, raise
re-raises the current exception in rescue clauses. Ruby's exception system integrates with the rescue
, ensure
, and retry
keywords to provide comprehensive error handling capabilities.
# Basic exception raising
raise "Something went wrong"
# Raising specific exception types
raise ArgumentError, "Invalid parameter value"
# Re-raising current exceptions
begin
risky_operation
rescue StandardError => e
log_error(e)
raise # Re-raises the caught exception
end
The fail
method behaves identically to raise
in all contexts and serves as a semantic alternative for indicating unrecoverable errors. Some Ruby style guides recommend using fail
for unrecoverable conditions and raise
for recoverable exceptions, though this distinction remains a matter of coding convention rather than language requirement.
Basic Usage
The raise
method supports multiple calling patterns depending on the level of exception detail required. The simplest form accepts a string message and creates a RuntimeError
instance:
def validate_age(age)
raise "Age must be positive" if age < 0
raise "Age must be reasonable" if age > 150
end
validate_age(-5) # Raises RuntimeError with custom message
Specifying exception classes provides more precise error categorization. Ruby accepts either the class itself followed by a message, or a pre-constructed exception instance:
class ConfigurationError < StandardError; end
def load_config(path)
raise ArgumentError, "Path cannot be nil" if path.nil?
raise ConfigurationError, "Invalid format" unless valid_format?(path)
# Alternative syntax with exception instances
raise Errno::ENOENT.new("Config file not found: #{path}") unless File.exist?(path)
end
The parameterless raise
form re-raises the currently handled exception, preserving the original backtrace and exception details. This pattern proves essential for logging or cleanup operations that need to maintain exception propagation:
def process_with_logging
begin
dangerous_operation
rescue => e
logger.error("Operation failed: #{e.message}")
cleanup_resources
raise # Preserves original exception and backtrace
end
end
Custom exception classes inherit from StandardError
or its subclasses and can accept initialization parameters for enhanced error context:
class ValidationError < StandardError
attr_reader :field, :value
def initialize(message, field: nil, value: nil)
super(message)
@field = field
@value = value
end
end
def validate_email(email)
raise ValidationError.new(
"Invalid email format",
field: :email,
value: email
) unless email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
end
Error Handling & Debugging
Exception handling in Ruby requires understanding the exception hierarchy and rescue clause behavior. Ruby matches rescue clauses against exception class inheritance, with more specific exceptions requiring placement before general ones:
def robust_file_operation(path)
begin
File.read(path).upcase
rescue Errno::ENOENT => e
logger.warn("File not found: #{path}")
raise FileNotFoundError, "Required file missing: #{path}"
rescue Errno::EACCES => e
logger.error("Permission denied: #{path}")
raise PermissionError, "Cannot access file: #{path}"
rescue IOError => e
logger.error("IO error reading #{path}: #{e.message}")
raise
rescue StandardError => e
logger.error("Unexpected error: #{e.class} - #{e.message}")
logger.error(e.backtrace.join("\n"))
raise ProcessingError, "File operation failed for #{path}"
end
end
Exception backtrace manipulation enables custom stack trace presentation and debugging information. The set_backtrace
method replaces exception backtraces, while caller
provides current execution context:
class DetailedError < StandardError
def initialize(message, context = {})
super(message)
@context = context
# Custom backtrace with context information
trace = caller(1, 10).map.with_index do |line, idx|
if idx < 3
"#{line} [Context: #{@context.inspect}]"
else
line
end
end
set_backtrace(trace)
end
end
def context_aware_operation(data)
context = { data_size: data.length, timestamp: Time.now }
raise DetailedError.new("Processing failed", context) if data.empty?
end
Exception cause chains track error propagation through multiple layers using the cause
attribute. Ruby automatically sets the cause when raising exceptions within rescue blocks:
def layered_processing
begin
database_operation
rescue DatabaseError => e
begin
fallback_operation
rescue FallbackError => fallback_e
# fallback_e.cause will be the original DatabaseError
raise ProcessingError, "All operations failed"
end
end
end
def analyze_error_chain(exception)
current = exception
depth = 0
while current
puts "Level #{depth}: #{current.class} - #{current.message}"
current = current.cause
depth += 1
break if depth > 10 # Prevent infinite loops
end
end
Thread-local exception handling prevents exceptions from crossing thread boundaries unexpectedly. Exceptions raised in threads remain contained unless explicitly propagated:
def concurrent_operations(tasks)
exceptions = Queue.new
threads = tasks.map do |task|
Thread.new do
begin
process_task(task)
rescue StandardError => e
exceptions << { task: task, exception: e }
end
end
end
threads.each(&:join)
# Check for exceptions after all threads complete
unless exceptions.empty?
failed_tasks = []
until exceptions.empty?
failure = exceptions.pop
failed_tasks << failure
end
raise ConcurrentProcessingError, "#{failed_tasks.length} tasks failed"
end
end
Common Pitfalls
Exception class hierarchy misunderstandings lead to rescue clauses that never execute. Ruby matches exceptions using inheritance, so rescuing StandardError
catches all application exceptions, potentially masking specific error types:
# Problematic: Specific rescue never executes
begin
operation_that_might_fail
rescue StandardError => e
handle_general_error(e)
rescue ArgumentError => e # Never reached!
handle_argument_error(e)
end
# Correct: Specific exceptions first
begin
operation_that_might_fail
rescue ArgumentError => e
handle_argument_error(e)
rescue StandardError => e
handle_general_error(e)
end
The fail
versus raise
semantic distinction causes confusion among developers. While both methods function identically, some style guides create arbitrary rules about their usage. Ruby treats them as perfect aliases:
# These are functionally identical
raise "Error occurred"
fail "Error occurred"
# Both support the same argument patterns
raise ArgumentError, "Invalid input"
fail ArgumentError, "Invalid input"
# Both can re-raise exceptions
rescue => e
log_error(e)
raise # or fail - identical behavior
end
Exception message interpolation with untrusted data creates security vulnerabilities and debugging challenges. String interpolation in exception messages can expose sensitive information or create injection vectors:
# Dangerous: Exposes sensitive data
def authenticate(username, password)
raise "Authentication failed for #{username} with password #{password}"
end
# Better: Generic messages with logging
def authenticate(username, password)
success = verify_credentials(username, password)
unless success
logger.warn("Authentication failed", user: username.hash)
raise AuthenticationError, "Invalid credentials"
end
end
Re-raising exceptions without preserving backtraces loses crucial debugging information. Creating new exceptions instead of using parameterless raise
destroys the original call stack:
# Wrong: Loses original backtrace
begin
risky_operation
rescue => e
cleanup_resources
raise StandardError, e.message # New backtrace starts here
end
# Correct: Preserves original backtrace
begin
risky_operation
rescue => e
cleanup_resources
raise # Maintains original exception and backtrace
end
# Alternative: Explicit backtrace preservation
begin
risky_operation
rescue => e
cleanup_resources
new_exception = ProcessingError.new("Operation failed: #{e.message}")
new_exception.set_backtrace(e.backtrace)
raise new_exception
end
Exception handling in ensure blocks creates double-exception scenarios where the ensure block raises exceptions that mask the original error:
# Dangerous: Ensure block can mask original exception
def process_file(path)
file = File.open(path)
begin
process_content(file.read) # Might raise ProcessingError
ensure
file.close # Might raise IOError, masking ProcessingError
end
end
# Safer: Protected cleanup
def process_file(path)
file = File.open(path)
begin
process_content(file.read)
ensure
begin
file.close if file
rescue IOError => cleanup_error
logger.error("Cleanup failed: #{cleanup_error.message}")
# Don't raise - preserve original exception
end
end
end
Production Patterns
Production exception handling requires structured logging, error aggregation, and graceful degradation strategies. Applications need comprehensive error tracking without exposing sensitive information to users:
class ProductionErrorHandler
def self.handle_request_error(error, request_context = {})
error_id = SecureRandom.uuid
# Structured logging for monitoring systems
logger.error({
error_id: error_id,
error_class: error.class.name,
message: error.message,
backtrace: error.backtrace&.first(10),
request_path: request_context[:path],
user_id: request_context[:user_id]&.hash,
timestamp: Time.now.iso8601
})
# External error tracking
ErrorTracker.notify(error, context: request_context.merge(error_id: error_id))
# User-friendly error response
case error
when ValidationError
{ status: 400, error: "Invalid input provided", reference: error_id }
when AuthenticationError
{ status: 401, error: "Authentication required", reference: error_id }
when AuthorizationError
{ status: 403, error: "Access denied", reference: error_id }
else
{ status: 500, error: "Internal server error", reference: error_id }
end
end
end
# Usage in web application
def process_api_request
begin
validate_input(params)
authorize_user(current_user)
perform_operation(params)
rescue StandardError => e
error_response = ProductionErrorHandler.handle_request_error(e, {
path: request.path,
user_id: current_user&.id,
params: sanitized_params
})
render json: error_response, status: error_response[:status]
end
end
Circuit breaker patterns prevent cascading failures by raising exceptions when external dependencies become unreliable. This approach protects system stability during partial outages:
class CircuitBreaker
FAILURE_THRESHOLD = 5
RECOVERY_TIMEOUT = 60
def initialize(name)
@name = name
@failure_count = 0
@last_failure_time = nil
@state = :closed # :closed, :open, :half_open
end
def call(&block)
case @state
when :open
if Time.now - @last_failure_time > RECOVERY_TIMEOUT
@state = :half_open
else
raise CircuitOpenError, "Circuit breaker #{@name} is open"
end
end
begin
result = block.call
reset if @state == :half_open
result
rescue => error
record_failure
raise
end
end
private
def record_failure
@failure_count += 1
@last_failure_time = Time.now
if @failure_count >= FAILURE_THRESHOLD
@state = :open
logger.warn("Circuit breaker #{@name} opened after #{@failure_count} failures")
end
end
def reset
@failure_count = 0
@last_failure_time = nil
@state = :closed
logger.info("Circuit breaker #{@name} reset")
end
end
# Usage with external service
payment_circuit = CircuitBreaker.new("payment_service")
begin
payment_circuit.call do
PaymentService.charge(amount, card_token)
end
rescue CircuitOpenError => e
logger.warn("Payment service unavailable: #{e.message}")
render json: { error: "Payment processing temporarily unavailable" }
rescue PaymentError => e
logger.error("Payment failed: #{e.message}")
render json: { error: "Payment could not be processed" }
end
Asynchronous error handling requires careful exception propagation across background job systems and message queues:
class BackgroundJobHandler
def self.perform_with_retry(job_class, *args, max_retries: 3)
attempt = 0
begin
attempt += 1
job_class.new.perform(*args)
rescue RetryableError => e
if attempt <= max_retries
delay = 2 ** attempt # Exponential backoff
logger.warn("Job #{job_class} failed (attempt #{attempt}/#{max_retries}), retrying in #{delay}s")
sleep(delay)
retry
else
logger.error("Job #{job_class} failed permanently after #{max_retries} attempts")
raise JobPermanentFailure.new(
"Job failed after #{max_retries} retries: #{e.message}",
original_error: e,
attempts: attempt
)
end
rescue NonRetryableError => e
logger.error("Job #{job_class} failed with non-retryable error: #{e.message}")
raise JobPermanentFailure.new(
"Job failed with non-retryable error: #{e.message}",
original_error: e,
attempts: attempt
)
end
end
end
# Background job with error classification
class EmailDeliveryJob
def perform(user_id, template, data)
user = User.find(user_id)
email = EmailTemplate.render(template, data)
begin
EmailService.deliver(user.email, email)
rescue EmailService::RateLimitError => e
raise RetryableError, "Rate limit exceeded: #{e.message}"
rescue EmailService::InvalidEmailError => e
raise NonRetryableError, "Invalid email address: #{user.email}"
rescue EmailService::ServiceUnavailableError => e
raise RetryableError, "Email service unavailable: #{e.message}"
end
end
end
Reference
Exception Hierarchy
Exception
├── NoMemoryError
├── ScriptError
│ ├── LoadError
│ ├── NotImplementedError
│ └── SyntaxError
├── SecurityError
├── SignalException
│ └── Interrupt
├── StandardError (default rescue target)
│ ├── ArgumentError
│ ├── EncodingError
│ ├── FiberError
│ ├── IOError
│ │ └── EOFError
│ ├── IndexError
│ │ ├── KeyError
│ │ └── StopIteration
│ ├── LocalJumpError
│ ├── NameError
│ │ └── NoMethodError
│ ├── RangeError
│ │ └── FloatDomainError
│ ├── RegexpError
│ ├── RuntimeError (default for raise with string)
│ ├── SystemCallError
│ │ └── Errno::* (system error codes)
│ ├── ThreadError
│ ├── TypeError
│ └── ZeroDivisionError
├── SystemExit
└── SystemStackError
Method Signatures
Method | Parameters | Returns | Description |
---|---|---|---|
raise |
None | Never returns | Re-raises current exception |
raise(message) |
message (String) |
Never returns | Raises RuntimeError with message |
raise(exception_class) |
exception_class (Class) |
Never returns | Raises exception with default message |
raise(exception_class, message) |
exception_class (Class), message (String) |
Never returns | Raises exception with custom message |
raise(exception_instance) |
exception_instance (Exception) |
Never returns | Raises the provided exception instance |
fail |
Same as raise | Never returns | Alias for raise method |
Exception Instance Methods
Method | Returns | Description |
---|---|---|
#message |
String | Exception message text |
#backtrace |
Array<String> or nil | Stack trace lines |
#backtrace_locations |
Array<Thread::Backtrace::Location> or nil | Backtrace location objects |
#cause |
Exception or nil | Exception that caused this exception |
#set_backtrace(trace) |
Array<String> | Sets custom backtrace |
#full_message |
String | Formatted exception with backtrace |
#inspect |
String | Technical representation |
#to_s |
String | String representation |
Common Exception Classes
Exception | Usage | Example Scenarios |
---|---|---|
ArgumentError |
Invalid method arguments | Wrong parameter count, invalid values |
RuntimeError |
General runtime errors | Default exception for raise with string |
TypeError |
Type-related errors | Method called on wrong type |
NameError |
Undefined variables/constants | Referencing undefined names |
NoMethodError |
Method not found | Calling non-existent methods |
IOError |
Input/output errors | File operations, network issues |
SystemCallError |
System-level errors | File permissions, resource limits |
StandardError |
Application errors | Base class for most exceptions |
Rescue Clause Patterns
# Basic rescue
begin
risky_code
rescue => e
handle_error(e)
end
# Multiple specific rescues
begin
risky_code
rescue ArgumentError => e
handle_argument_error(e)
rescue IOError => e
handle_io_error(e)
rescue => e
handle_general_error(e)
end
# Rescue with retry
begin
risky_code
rescue RetryableError => e
retry_count ||= 0
if (retry_count += 1) < 3
sleep(retry_count)
retry
else
raise
end
end
# Rescue with ensure
begin
risky_code
rescue => e
handle_error(e)
raise
ensure
cleanup_code
end
Exception Raising Patterns
Pattern | Code Example | Use Case |
---|---|---|
Simple message | raise "Error occurred" |
Quick error signaling |
Typed exception | raise ArgumentError, "Invalid input" |
Specific error classification |
Custom exception | raise CustomError.new(details) |
Rich error context |
Re-raising | raise (in rescue block) |
Error propagation |
Conditional raising | raise "Error" if condition |
Guard clauses |
Exception chaining | raise NewError, "Failed" rescue OldError |
Error context layering |