rubyguides

Ruby error handling with begin, rescue, ensure, and raise

Every Ruby program that touches the outside world—a file, a network request, a database query—will eventually face an exception. Ruby error handling is how you keep that program alive when something unexpected happens. A request can fail, a file can be missing, a number can be invalid, or an external service can go away for a moment. When that happens, Ruby raises an exception. The question is not whether exceptions exist, but how your code should respond.

Intro context

If you are new to exceptions, the easiest way to think about Ruby error handling is as a controlled detour. Instead of letting the program stop at the first failure, you tell Ruby which parts of the code might fail, what should happen when they do, and what cleanup should still run afterwards.

That matters because real applications rarely run in perfect conditions. Users enter bad data, networks time out, files disappear, and databases get busy. Good error handling turns those situations into predictable outcomes instead of surprises.

TL;DR

  • Use begin and rescue to catch exceptions where they are likely to happen.
  • Rescue specific exception classes instead of rescuing everything.
  • Use ensure for cleanup that must run no matter what.
  • Use raise when your own code needs to stop with a clear message.
  • Keep error handling close to the place where recovery makes sense.

Ruby error handling: the begin and rescue pattern

Ruby uses begin to mark code that might fail, and rescue to catch the exception if it does. This is the most common pattern you will use when a failure is expected and you want to keep control of the program.

begin
  result = 10 / 0
rescue
  puts "Something went wrong!"
end

Here, 10 / 0 raises ZeroDivisionError, so Ruby jumps to the rescue block. The key idea is that your program does not crash by accident. You decide what message to show, what fallback value to use, or whether to try a different path.

Why this matters

Rescuing a specific exception class like ZeroDivisionError is better than a bare rescue because it tells the reader exactly what you expect to fail. A bare rescue catches everything, including syntax errors and interrupt signals, which can hide bugs that should crash the program loudly. Specific rescues keep the safety net narrow enough that real problems still surface, while giving you control over the failures you anticipated.

Without error handling, the same code stops the program immediately:

result = 10 / 0
# => ZeroDivisionError: divided by 0

Handling this error means your program can keep running instead of stopping abruptly. You might choose a default value, log the failure, or redirect the user to a fallback path. The power of rescue is that you decide the outcome, rather than letting the exception propagate all the way up and crash the process.

With error handling, you can choose a safer outcome:

begin
  result = 10 / 0
rescue ZeroDivisionError
  puts "Cannot divide by zero."
  result = 0
end

puts result

That is a small example, but the same pattern shows up in file reads, API requests, database work, and form processing.

Handling specific exception types

Ruby gives you many exception classes, and that is useful because not every failure should be treated the same way. A file that is missing is different from a file that you are not allowed to read. A wrong method call is different from a network timeout.

begin
  file = File.open("nonexistent.txt")
  content = file.read
rescue Errno::ENOENT
  puts "File not found."
rescue Errno::EACCES
  puts "Permission denied."
end

Rescuing particular exception classes keeps the code transparent about its expectations. You can show the right message, retry only when it makes sense, and avoid hiding unrelated bugs. If you rescue too broadly, your program may appear to keep working while a real problem stays buried.

Common exception types

ExceptionWhen it occurs
StandardErrorMost application errors inherit from this class
ArgumentErrorA method gets the wrong arguments
NameErrorRuby cannot find a variable or method
TypeErrorA value is the wrong type for the operation
ZeroDivisionErrorCode divides by zero
NoMethodErrorCode calls a method that does not exist

Most application code rescues specific StandardError subclasses. Avoid rescuing everything unless you truly need a very broad safety net.

How do you store and inspect the exception object?

You often want the exception object itself, not just a generic message. Ruby lets you capture it in a variable so you can inspect the class, message, or backtrace.

begin
  File.open("missing.txt")
rescue Errno::ENOENT => e
  puts "Error: #{e.message}"
  puts "Class: #{e.class}"
end

That is useful when you need to log the failure or show a user-friendly message while still keeping the technical details for debugging. The exception object can tell you what went wrong and where Ruby noticed it first.

Using ensure for cleanup

Some code has to run whether the block succeeds or fails. Closing a file, releasing a resource, or ending a transaction are classic examples. That is what ensure is for.

file = File.open("data.txt", "w")
begin
  file.write("Hello")
  raise "Something went wrong!"
rescue
  puts "Error occurred: #{$!}"
ensure
  file.close
end

The ensure block gives you a safe place for cleanup. It runs after the begin block, even if an exception is raised and rescued. That makes it a good fit for anything that must happen exactly once, no matter how the main work ends.

A real-world example

def fetch_users
  db = Database.connect
  begin
    db.query("SELECT * FROM users")
  rescue DatabaseError => e
    puts "Database error: #{e.message}"
    []
  ensure
    db.close if db
  end
end

This pattern keeps the connection lifecycle explicit. The query can fail, but the connection still closes cleanly.

Using retry to try again

Some failures are temporary, such as a short network timeout or a service that responds slowly for a moment. In those cases, you can retry the block after handling the exception.

begin
  response = HTTP.get("https://api.example.com/data")
rescue Timeout::Error
  puts "Request timed out, retrying..."
  retry
end

retry is powerful, but it needs limits. If the failure is permanent, retrying forever only hides the problem and burns time. Put a cap on the number of attempts or switch to a fallback path after a few tries.

The key decision with retry is how many attempts are reasonable. For a network timeout, three attempts with a short delay between each one is usually enough. For a database deadlock, a single retry might be all you need because the conflict resolution is handled by the database itself. The important thing is to always have a path that stops retrying—whether that means raising a different error, returning a default value, or logging the failure for manual review.

attempt = 0
begin
  attempt += 1
  response = HTTP.get("https://api.example.com/data")
rescue Timeout::Error
  if attempt < 3
    puts "Retry #{attempt}"
    retry
  else
    puts "Failed after 3 attempts."
  end
end

That gives you a simple pattern for resilience without creating an endless loop.

Raising your own exceptions

You do not have to wait for Ruby to fail on its own. When your own method sees invalid input or an impossible state, use raise to stop early with a clear message.

def withdraw(amount)
  raise ArgumentError, "Amount must be positive" if amount <= 0
  raise InsufficientFundsError, "Not enough money" if amount > balance
  # ... process withdrawal
end

Custom exceptions make your intent easier to read. They also let callers rescue exactly the problem they care about without catching unrelated failures. A custom exception class gives the error a name that describes the domain, such as PaymentFailedError or ValidationError, which means the caller can decide how to handle each kind of failure independently.

class MyCustomError < StandardError
end

raise MyCustomError, "Something went wrong!"

When to use error handling

Choosing between a conditional check and a rescue block comes down to predictability. If you can easily test for the condition ahead of time, a conditional keeps the code path obvious and avoids the overhead of exception handling. If the failure is truly unexpected, a rescue block is cleaner because it separates the happy path from the recovery path.

# Prefer a conditional when the situation is predictable:
name = user.name if user.respond_to?(:name)

# Use rescue when the failure is exceptional:
name = user.fetch(:name)
rescue KeyError
  "unknown"

Use error handling when a failure is possible and recovery is meaningful. That often includes file access, database work, HTTP calls, and user input that must be validated after the fact.

Use simple conditionals when the situation is expected and easy to check first. For example, if a value may be nil, it is often clearer to test for nil before doing the work. Error handling is best when the failure is exceptional, not when it is the normal path.

Another good rule is to handle the error as close to the problem as practical. If a method can recover locally, do it there. If the caller needs to decide what to do, let the exception bubble up and handle it one level higher.

Frequently asked questions

Should I rescue Exception?

Usually no. Exception is too broad and can hide serious problems. Rescue specific subclasses of StandardError instead, unless you have a very unusual need.

Is ensure the same as finally?

Yes, conceptually. Ruby uses ensure where other languages use finally. It is the block that always runs after the main work finishes.

When should I use retry?

Use it for temporary failures that may succeed on a second or third try. Always add a limit so the program can stop if the problem never clears up.

How do I choose the right exception class?

Pick the most specific class that matches the problem. A specific exception makes the code easier to rescue, log, and reason about later.

Summary

Ruby error handling is about making failure predictable so that programs stay alive even when conditions change unexpectedly. begin marks the risky work, rescue handles the exception, ensure takes care of cleanup, and raise lets your own code fail loudly when something is invalid.

The most useful habit is to rescue the smallest exception you can. That keeps bugs visible, lets recovery stay focused, and makes the rest of the program easier to trust.

Once you are comfortable with Ruby error handling, the next step is to apply the same mindset to file operations. File I/O is a practical place to use begin, rescue, and ensure because files can go missing, be locked, or run out of space.

See Also