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
beginandrescueto catch exceptions where they are likely to happen. - Rescue specific exception classes instead of rescuing everything.
- Use
ensurefor cleanup that must run no matter what. - Use
raisewhen 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
| Exception | When it occurs |
|---|---|
StandardError | Most application errors inherit from this class |
ArgumentError | A method gets the wrong arguments |
NameError | Ruby cannot find a variable or method |
TypeError | A value is the wrong type for the operation |
ZeroDivisionError | Code divides by zero |
NoMethodError | Code 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.
Forward link
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
- File I/O in Ruby - file work is one of the most common places to use
begin,rescue, andensure - Ruby control flow - learn how
if,case, and loops fit alongside error handling - Ruby strings - useful when building friendly error messages or sanitising input
- Ruby exception handling docs - official Ruby class reference