Exception handling is a crucial part of any programming language, and Ruby is no exception. It allows you to manage errors gracefully and ensure that your program can handle unexpected situations without crashing.

Key Concepts

  1. Exceptions: An exception is an event that disrupts the normal flow of a program. In Ruby, exceptions are objects that are instances of the Exception class or its subclasses.
  2. Raising Exceptions: You can raise exceptions using the raise keyword.
  3. Rescuing Exceptions: You can handle exceptions using the begin-rescue-end block.
  4. Ensuring Execution: The ensure block allows you to run code regardless of whether an exception was raised or not.
  5. Custom Exceptions: You can define your own exception classes by inheriting from the StandardError class.

Raising Exceptions

You can raise an exception using the raise keyword. Here’s a simple example:

def divide(a, b)
  raise "Division by zero error" if b == 0
  a / b
end

begin
  puts divide(10, 0)
rescue => e
  puts "An error occurred: #{e.message}"
end

Explanation

  • raise "Division by zero error": Raises an exception with the message "Division by zero error" if b is zero.
  • begin-rescue-end block: This block is used to handle exceptions. If an exception is raised within the begin block, the rescue block will catch it.
  • rescue => e: Captures the exception object in the variable e.
  • e.message: Retrieves the error message from the exception object.

Rescuing Specific Exceptions

You can rescue specific exceptions by specifying the exception class:

begin
  File.open("non_existent_file.txt")
rescue Errno::ENOENT => e
  puts "File not found: #{e.message}"
end

Explanation

  • rescue Errno::ENOENT => e: This line rescues only Errno::ENOENT exceptions, which are raised when a file is not found.

Ensuring Execution

The ensure block runs code regardless of whether an exception was raised or not:

begin
  file = File.open("example.txt", "w")
  file.write("Hello, world!")
rescue => e
  puts "An error occurred: #{e.message}"
ensure
  file.close if file
end

Explanation

  • ensure block: Ensures that the file is closed whether an exception occurs or not.

Custom Exceptions

You can create custom exceptions by subclassing StandardError:

class CustomError < StandardError; end

def risky_method
  raise CustomError, "Something went wrong!"
end

begin
  risky_method
rescue CustomError => e
  puts "Caught a custom error: #{e.message}"
end

Explanation

  • class CustomError < StandardError; end: Defines a custom exception class.
  • raise CustomError, "Something went wrong!": Raises an instance of the custom exception.
  • rescue CustomError => e: Catches the custom exception.

Practical Exercises

Exercise 1: Basic Exception Handling

Write a method safe_divide that takes two arguments and returns their division. If the second argument is zero, it should raise an exception with the message "Cannot divide by zero".

def safe_divide(a, b)
  # Your code here
end

begin
  puts safe_divide(10, 2)  # Should print 5
  puts safe_divide(10, 0)  # Should raise an exception
rescue => e
  puts "Error: #{e.message}"
end

Solution

def safe_divide(a, b)
  raise "Cannot divide by zero" if b == 0
  a / b
end

begin
  puts safe_divide(10, 2)  # Should print 5
  puts safe_divide(10, 0)  # Should raise an exception
rescue => e
  puts "Error: #{e.message}"
end

Exercise 2: Custom Exception

Create a custom exception NegativeNumberError and write a method square_root that takes a number and returns its square root. If the number is negative, it should raise the custom exception.

class NegativeNumberError < StandardError; end

def square_root(number)
  # Your code here
end

begin
  puts square_root(9)   # Should print 3.0
  puts square_root(-1)  # Should raise NegativeNumberError
rescue NegativeNumberError => e
  puts "Error: #{e.message}"
end

Solution

class NegativeNumberError < StandardError; end

def square_root(number)
  raise NegativeNumberError, "Cannot take the square root of a negative number" if number < 0
  Math.sqrt(number)
end

begin
  puts square_root(9)   # Should print 3.0
  puts square_root(-1)  # Should raise NegativeNumberError
rescue NegativeNumberError => e
  puts "Error: #{e.message}"
end

Common Mistakes and Tips

  • Not rescuing specific exceptions: Always try to rescue specific exceptions rather than using a generic rescue clause. This makes your code more robust and easier to debug.
  • Forgetting the ensure block: Use the ensure block to release resources like file handles or database connections.
  • Raising exceptions unnecessarily: Only raise exceptions for truly exceptional conditions. Overusing exceptions can make your code harder to read and maintain.

Conclusion

In this section, you learned how to handle exceptions in Ruby using the raise, rescue, and ensure keywords. You also learned how to create custom exceptions to handle specific error conditions. Exception handling is a powerful tool that helps you write robust and error-resistant code. In the next section, we will dive into file input/output operations in Ruby.

© Copyright 2024. All rights reserved