Error Handling and Performance Optimization — Ruby Deep Dive [05]

Bhavyansh @ DiversePixel
4 min readJul 22, 2024

--

Error Handling

Effective error handling is crucial for building robust applications. Ruby provides several mechanisms to manage errors gracefully.

Basic Error Handling with begin, rescue, ensure, and else

Ruby’s begin block is used to handle exceptions that might occur during execution. Here's a breakdown of how it works:

  • begin: Starts the block of code to be monitored for exceptions.
  • rescue: Specifies the code to execute if an exception occurs.
  • ensure: Contains code that will always run, regardless of whether an exception was raised.
  • else: Runs code if no exceptions were raised in the begin block.

Example:

begin
# Code that may raise an exception
result = 10 / 0
rescue ZeroDivisionError => e
# Code to handle the exception
puts "Error: #{e.message}"
else
# Code that runs if no exception occurs
puts "Result is #{result}"
ensure
# Code that always runs
puts "This code runs no matter what"
end

Custom Exceptions

You can define custom exceptions by subclassing StandardError or any of its descendants. This allows you to create meaningful error types specific to your application.

Example:

class CustomError < StandardError; end
begin
raise CustomError, "Something went wrong!"
rescue CustomError => e
puts "Caught a custom error: #{e.message}"
end

Best Practices for Error Handling

  1. Rescue Specific Errors: Avoid rescuing generic Exception class. Instead, rescue specific errors.
  2. Use Ensure for Cleanup: Always use ensure to clean up resources, such as closing files or releasing memory.
  3. Log Errors: Ensure that all errors are logged for debugging and monitoring.
  4. Fail Fast: Catch and handle errors at the point they occur rather than letting them propagate.
  5. Avoid Silent Failures: Avoid rescuing errors without handling them, which can lead to silent failures.

Performance Optimization

Optimizing performance is essential for building efficient and scalable applications. Ruby provides various tools and techniques for performance tuning.

Profiling and Benchmarking Tools

  1. Benchmark Module: Ruby’s Benchmark module helps measure and report the execution time of code.

Example:

require 'benchmark'
Benchmark.bm do |x|
x.report("Code block 1:") { sleep(1) }
x.report("Code block 2:") { sleep(2) }
end
  1. RubyProf: A fast profiler for Ruby that can measure time and memory usage.
  2. StackProf: A sampling call-stack profiler for Ruby, useful for finding performance bottlenecks.

Common Performance Pitfalls and How to Avoid Them

  1. Avoid N+1 Queries: Ensure database queries are optimized to avoid executing multiple similar queries. This occurs when you fetch a collection of objects and then run a separate query for each object in the collection, resulting in N+1 database queries.

Solutions to avoid N+1 queries:

i. Eager Loading: Use includes, preload, or eager_load methods to load associated data in advance. Example:

# Instead of:
posts = Post.all
posts.each { |post| puts post.author.name }
# Use:
posts = Post.includes(:author).all
posts.each { |post| puts post.author.name }

ii. Batch Loading: Use libraries like batch-loader for more complex scenarios.

iii. Counter Cache: For counting associated records, use counter caches to avoid queries.

iv. Joining Tables: Use joins when you only need data from the joined table for filtering or ordering.

v. Bullet Gem: Use the Bullet gem in development to detect N+1 queries.

vi. Custom SQL: Write custom SQL for complex queries that ORMs struggle to optimize.

2. Use Lazy Enumerables: Use lazy enumerables for large collections to avoid loading all elements into memory. Lazy enumerables allow you to work with potentially infinite collections without loading everything into memory at once. They’re especially useful for large datasets.

Example:

# Instead of:
(1..Float::INFINITY).select { |n| n % 2 == 0 }.take(10)
# Use:
(1..Float::INFINITY).lazy.select { |n| n % 2 == 0 }.take(10).force

The lazy method creates a lazy enumerator, which only processes elements as needed.

3. Optimize Loops: Avoid unnecessary loops and prefer built-in methods that are optimized. Ruby provides many built-in methods that are more efficient than traditional loops for common operations.

Examples:

a) Use each instead of for:

# Instead of:
for i in 0...array.length
puts array[i]
end
# Use:
array.each { |item| puts item }

b) Use map for transformations:

# Instead of:
new_array = []
array.each { |item| new_array << item * 2 }# Use:
new_array = array.map { |item| item * 2 }

c) Use select or reject for filtering:

# Instead of:
even_numbers = []
numbers.each { |n| even_numbers << n if n.even? }
# Use:
even_numbers = numbers.select(&:even?)

d) Use reduce for aggregations:

# Instead of:
sum = 0
numbers.each { |n| sum += n }
# Use:
sum = numbers.reduce(:+)

e) Use each_with_index when you need both the item and its index:

# Instead of:
array.each_with_index do |item, index|
puts "#{index}: #{item}"
end
# Use:
array.each_with_index { |item, index| puts "#{index}: #{item}" }

These optimizations can significantly improve performance and memory usage, especially when dealing with large datasets.

Memory Management Tips

  1. Use Symbols Wisely: Symbols are not garbage collected. Use them only when necessary.
  2. String Mutations: Prefer string mutations (e.g., <<) over creating new strings (e.g., +=).
  3. Object Pooling: Reuse objects where possible to reduce memory allocation overhead.

Example:

# Using << for string concatenation
str = "Hello"
str << " World" # More efficient than str += " World"

Optimization Techniques

  1. Caching: Cache expensive computations or frequently accessed data to improve performance.
  2. Efficient Algorithms: Choose the most efficient algorithm for the task at hand. Analyze the time and space complexity.
  3. Concurrency and Parallelism: Utilize Ruby’s threading and forking capabilities to perform tasks concurrently or in parallel.

Example:

# Caching example
require 'memoist'
class Calculator
extend Memoist
def expensive_computation(n)
sleep(2) # Simulate a time-consuming task
n ** 2
end
memoize :expensive_computation
end
calculator = Calculator.new
puts calculator.expensive_computation(4) # Cached result for subsequent calls

--

--

Bhavyansh @ DiversePixel
Bhavyansh @ DiversePixel

Written by Bhavyansh @ DiversePixel

Hey I write about Tech. Join me as I share my tech learnings and insights. 🚀

No responses yet