Generación y rescate de excepciones en Ruby

Cómo lanzar y rescatar excepciones en Ruby

Exceptions are a fundamental part of programming in Ruby, allowing developers to handle errors gracefully and ensure robust, fault-tolerant applications. Ruby’s exception-handling mechanism is intuitive yet powerful, enabling developers to raise errors when something goes wrong and rescue them to prevent application crashes. In this 2000-word guide, we’ll explore how to raise and rescue exceptions in Ruby, covering the basics, advanced techniques, best practices, and real-world examples.

What Are Exceptions in Ruby?

Exceptions in Ruby are objects that represent errors or unexpected conditions during program execution. When an error occurs—such as attempting to divide by zero, accessing a nonexistent file, or encountering a network failure—Ruby raises an exception. If not handled, the exception causes the program to terminate with an error message.

Ruby’s exception system is built around the Exception class, which serves as the root of the exception hierarchy. Subclasses like StandardError, RuntimeError, ArgumentError, y NoMethodError handle specific types of errors. Developers can also define custom exception classes to represent application-specific errors.

Exception handling in Ruby revolves around two key actions:

  • Raising: Triggering an exception when an error occurs.
  • Rescuing: Catching and handling exceptions to prevent program crashes.

Let’s dive into how to raise and rescue exceptions effectively.

Raising Exceptions in Ruby

Raising an exception is the process of signaling that an error or unexpected condition has occurred. Ruby provides the raise method (and its alias fail) to trigger exceptions.

En raise Método

The simplest way to raise an exception is to use the raise method with no arguments, which raises a RuntimeError (a subclass of StandardError):

ruby
raise
# => RuntimeError: unhandled exception

You can also provide an error message:

ruby
raise "Something went wrong!"
# => RuntimeError: Something went wrong!

To raise a specific exception class, pass the class as the first argument and the message as the second:

ruby
raise ArgumentError, "Invalid input provided"
# => ArgumentError: Invalid input provided

Custom Exception Classes

For more complex applications, you may want to define custom exception classes to represent specific errors. Custom exceptions inherit from StandardError or one of its subclasses to ensure compatibility with Ruby’s default rescue behavior.

Ejemplo:

ruby
class AuthenticationError < StandardError; end

def login(username, password)
  raise AuthenticationError, "Invalid credentials" unless valid_credentials?(username, password)
  puts "Login successful!"
end

def valid_credentials?(username, password)
  username == "admin" && password == "secret"
end

login("user", "wrong") # => AuthenticationError: Invalid credentials

By defining AuthenticationError, you can handle authentication-related errors separately from generic errors.

Raising Exceptions with Cause

Ruby allows you to attach a “cause” to an exception, which is useful for debugging. The cause is the original exception that led to the current one. Use the exception method to access it:

ruby
begin
  raise "Original error"
rescue => e
  raise "New error" # The original error is preserved as the cause
end

You can inspect the cause with e.cause:

ruby
begin
  begin
    raise "Original error"
  rescue => e
    raise "New error"
  end
rescue => e
  puts e.message       # => New error
  puts e.cause.message # => Original error
end

Rescuing Exceptions

Rescuing exceptions allows you to catch and handle errors gracefully, preventing your program from crashing. Ruby uses the begin/rescue block to manage exceptions.

En comenzar/rescue Block

The basic structure of a begin/rescue block is:

ruby
begin
  # Code that might raise an exception
rescue
  # Handle the exception
end

Ejemplo:

ruby
begin
  result = 10 / 0
rescue
  puts "An error occurred!"
end
# Output: An error occurred!

Por defecto, rescue catches StandardError and its subclasses. If you don’t specify an exception class, it’s equivalent to rescue StandardError.

Handling Specific Exceptions

To handle specific exceptions, specify the exception class in the rescue clause:

ruby
begin
  result = 10 / 0
rescue ZeroDivisionError
  puts "Cannot divide by zero!"
rescue ArgumentError
  puts "Invalid argument provided!"
end
# Output: Cannot divide by zero!

You can also capture the exception object for further inspection:

ruby
begin
  raise ArgumentError, "Invalid input"
rescue ArgumentError => e
  puts "Error: #{e.message}"
end
# Output: Error: Invalid input

Utilizando demás y ensure

Ruby provides two additional clauses for exception handling:

  • demás: Executes if no exception is raised.
  • ensure: Executes regardless of whether an exception occurs, useful for cleanup tasks.

Ejemplo:

ruby
begin
  puts "Performing operation..."
  result = 10 / 2
rescue ZeroDivisionError
  puts "Cannot divide by zero!"
else
  puts "Operation successful: #{result}"
ensure
  puts "Cleaning up..."
end
# Output:
# Performing operation...
# Operation successful: 5
# Cleaning up...

If an exception occurs:

ruby
begin
  puts "Performing operation..."
  result = 10 / 0
rescue ZeroDivisionError
  puts "Cannot divide by zero!"
else
  puts "Operation successful: #{result}"
ensure
  puts "Cleaning up..."
end
# Output:
# Performing operation...
# Cannot divide by zero!
# Cleaning up...

En retry Keyword

En retry keyword allows you to retry the comenzar block after an exception is caught. This is useful for scenarios like retrying failed network requests.

Ejemplo:

ruby
attempts = 0
begin
  attempts += 1
  puts "Attempt #{attempts}"
  raise "Connection failed"
rescue
  retry if attempts < 3
  puts "Giving up after #{attempts} attempts"
end
# Output:
# Attempt 1
# Attempt 2
# Attempt 3
# Giving up after 3 attempts

Use retry cautiously to avoid infinite loops.

Best Practices for Exception Handling

  1. Rescue Specific Exceptions: Avoid bare rescue clauses, as they catch all StandardError subclasses and can hide unexpected errors. Specify the exact exceptions you expect.
    ruby
    # Bad
    begin
      # Code
    rescue
      # Catches everything
    end
    
    # Good
    begin
      # Code
    rescue ArgumentError, TypeError
      # Handle specific errors
    end
  2. Keep Rescue Blocks Small: Only wrap the code that might raise an exception. This improves readability and prevents catching unrelated errors.
  3. Provide Meaningful Error Messages: When raising exceptions, include clear, actionable messages to aid debugging.
  4. Use Custom Exceptions for Domain Logic: Create custom exception classes for application-specific errors to make your code more expressive and maintainable.
  5. Avoid Overusing Exceptions for Flow Control: Exceptions are for exceptional cases, not for controlling program flow. Use conditionals for expected scenarios.
    ruby
    # Bad
    begin
    value = hash[:key]
    rescue
    value = nil
    end
    
    # Good
    value = hash[:key] || nil
  6. Clean Up Resources with ensure: Use ensure to close files, database connections, or other resources, even if an exception occurs.

Real-World Examples

File Handling

Reading a file can raise exceptions like Errno::ENOENT (file not found) or Errno::EACCES (permission denied). Here’s how to handle them:

ruby
begin
  File.open("nonexistent.txt", "r") do |file|
    puts file.read
  end
rescue Errno::ENOENT
  puts "File not found!"
rescue Errno::EACCES
  puts "Permission denied!"
ensure
  puts "File operation complete."
end
# Output: File not found!
# File operation complete.

API Calls

When making HTTP requests, you might encounter network errors or invalid responses. Using the httparty gem:

ruby
require 'httparty'

begin
  response = HTTParty.get('https://api.example.com/data')
rescue HTTParty::Error => e
  puts "API request failed: #{e.message}"
rescue SocketError
  puts "Network error: Could not connect to server"
else
  puts "Received response: #{response.body}"
end

Custom Exception Handling in a Class

Here’s an example of a class that processes payments and uses custom exceptions:

ruby
class PaymentError < StandardError; end
class InsufficientFundsError < PaymentError; end
class InvalidCardError < PaymentError; end

class PaymentProcessor
  def process_payment(amount, card)
    raise InvalidCardError, "Card is invalid" unless valid_card?(card)
    raise InsufficientFundsError, "Not enough funds" if amount > card.balance
    card.balance -= amount
    puts "Payment of #{amount} processed successfully"
  end

  private
  def valid_card?(card)
    card.number.length == 16
  end
end

class Card
  attr_accessor :number, :balance
  def initialize(number, balance)
    @number = number
    @balance = balance
  end
end

card = Card.new("1234567890123456", 50)
processor = PaymentProcessor.new

begin
  processor.process_payment(100, card)
rescue InsufficientFundsError => e
  puts "Error: #{e.message}"
rescue InvalidCardError => e
  puts "Error: #{e.message}"
end
# Output: Error: Not enough funds

Advanced Exception Handling

Nested Rescues

You can nest begin/rescue blocks to handle exceptions at different levels:

ruby
begin
  begin
    raise "Inner error"
  rescue
    puts "Caught inner error"
    raise "Outer error"
  end
rescue
  puts "Caught outer error"
end
# Output:
# Caught inner error
# Caught outer error

Exception Hierarchy

Understanding Ruby’s exception hierarchy is crucial. Key classes include:

  • ExceptionM: Root class for all exceptions.
  • StandardError: Default for rescue without a class; most built-in exceptions inherit from it.
  • RuntimeError: Default for raise without a class.
  • NoMethodError, ArgumentError, TypeError, etc.: Specific error types.

To catch all exceptions (including non-StandardError ones like SystemExit), use rescue Exception:

ruby
begin
  exit
rescue Exception
  puts "Caught exit"
end
# Output: Caught exit

Utilizando rescue_from in Rails

In Ruby on Rails, you can use rescue_from in controllers to handle exceptions globally:

ruby
class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, with: :not_found

  private

  def not_found
    render file: 'public/404.html', status: :not_found
  end
end

This approach centralizes exception handling for specific controllers.

Common Pitfalls to Avoid

  1. Catching All Exceptions Blindly: Using rescue without specifying an exception class can hide bugs.
  2. Overusing retry: Retrying indefinitely can lead to infinite loops or mask underlying issues.
  3. Ignoring Exception Details: Always inspect the exception object (e.message, e.backtrace) for debugging.
  4. Raising Non-Standard Errors: Avoid raising exceptions that don’t inherit from StandardError, as they won’t be caught by default rescue clauses.
  5. Not Cleaning Up Resources: Forgetting to use ensure can leave files or connections open.

Conclusión

Raising and rescuing exceptions in Ruby is a powerful way to handle errors and build robust applications. At RielesCarma, a leading Empresa de desarrollo de Ruby on Rails, we leverage these techniques to create reliable, maintainable solutions. By using raise to signal errors, rescue to catch them, and tools like retry, else, y ensure, developers can manage errors effectively. Custom exception classes and specific rescue clauses add clarity and precision to your code. Following best practices—such as rescuing specific exceptions, keeping rescue blocks small, and using meaningful error messages—ensures maintainable and reliable code.

Whether you’re handling file operations, API calls, or domain-specific logic, Ruby’s exception-handling system provides the flexibility to address errors gracefully. By mastering these techniques and avoiding common pitfalls, RailsCarma helps businesses build resilient Ruby applications that handle errors with confidence.

Artículos Relacionados

Acerca del autor de la publicación

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *


es_ESSpanish