CrackedRuby logo

CrackedRuby

raise and fail

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