
To create a custom exception in Ruby, define a class that inherits from StandardError: class PaymentError < StandardError; end. Raise it with raise PaymentError, "card declined" and rescue it by class. Inherit from StandardError, not Exception, so a bare rescue still catches it. Pass a default message by overriding initialize and calling super.
Ruby’s built-in exception classes describe the language’s failure modes, not your application’s. A PaymentError tells you more than a RuntimeError ever will — and when an error report lands in your logs or your debugging tooling, the class name is the first thing you read.
This article defines custom exception classes on Ruby 3.4, gives them useful messages and contextual data, and organizes them into hierarchies your rescue clauses can work with.
How to Define a Custom Exception in Ruby
Almost everything in Ruby is an object, and errors are no exception. A custom exception is an ordinary class that inherits from StandardError — one line is a complete, working error type:
class PaymentError < StandardError; endraise accepts the class with an optional message string:
class PaymentError < StandardError; end
raise PaymentError, "card declined"Uncaught, this stops the program and prints the message with your class name:
pay.rb:3:in '<main>': card declined (PaymentError)
raise PaymentError.new("card declined") — passing an exception instance instead of a class and message — produces exactly the same result. The two-argument form is the common idiom; the instance form is handy when you build the exception in one place and raise it in another.
A custom exception is rescued by class name, like any built-in error:
begin
charge_customer # Any code that can raise PaymentError.
rescue PaymentError => e
puts e.message
endWhy Inherit From StandardError, Not Exception
Exception is the root of Ruby’s exception tree, but it is the wrong superclass for application errors. An abridged view of the Ruby 3.4 hierarchy shows why:
Exception
├── NoMemoryError
├── SignalException
│ └── Interrupt
├── SystemExit
└── StandardError
├── ArgumentError
├── NameError
│ └── NoMethodError
├── RuntimeError
└── TypeError
The classes that descend directly from Exception signal events your code usually shouldn’t intercept: Interrupt is Ctrl-C, SystemExit is exit doing its job. Application-level failures all live under StandardError, and that’s the branch Ruby’s rescue machinery is built around: a bare rescue clause — one that names no class — catches StandardError and its descendants.
begin
raise PaymentError, "card declined"
rescue => e
puts e.class # PaymentError
endBecause PaymentError inherits from StandardError, the bare rescue catches it. Subclass Exception directly and your error slips past every normal rescue block in the codebase:
class CriticalError < Exception; end
begin
raise CriticalError, "internal failure"
rescue => e
puts "rescued" # never printed
endcrit.rb:4:in '<main>': internal failure (CriticalError)
The rescue clause never runs, and the program crashes. Unless you are building something that genuinely must escape normal error handling, inherit from StandardError — or from a more specific built-in subclass like ArgumentError when your error refines its meaning. The full class list is in the Ruby 3.4 Exception docs, and our guide to rescuing exceptions in Ruby covers the rescue side in depth — multiple classes per clause, retries, and why rescue Exception causes trouble.
Custom Error Messages: super, message, and Defaults
Raise a custom exception without a message, and Ruby uses the class name as the message:
class PaymentError < StandardError; end
raise PaymentErrorpay.rb:3:in '<main>': PaymentError (PaymentError)
For an error with a sensible default, override initialize with a default argument and hand the message to super:
class PaymentError < StandardError
def initialize(msg = "payment could not be processed")
super
end
end
begin
raise PaymentError
rescue => e
puts e.message # payment could not be processed
end
begin
raise PaymentError, "card declined"
rescue => e
puts e.message # card declined
endThe bare super passes msg along to StandardError, so callers can still override the default with their own message.
The alternative is overriding the message method itself:
class GatewayError < StandardError
def message
"the payment gateway did not respond"
end
end
begin
raise GatewayError, "this message is ignored"
rescue => e
puts e.message # the payment gateway did not respond
endA message override always wins — even a message passed at the raise site is discarded. Reach for it when the message should be fixed; use initialize plus super when the default should be overridable.
Adding Context to a Custom Exception
An exception is a full Ruby object, so it can carry structured data alongside its message — an error code, the record involved, the original failure. Define readers for the extra attributes and store them in initialize:
class CustomException < StandardError
attr_reader :code, :details
def initialize(code, msg, details)
@code = code
@details = details
super(msg)
end
endThe super(msg) call is load-bearing. Skip it, and the message you pass in never reaches Exception’s internals — e.message falls back to the class name and returns "CustomException", exactly the bug an earlier version of this article shipped. When initialize takes more than one argument, call super(msg) explicitly with the message alone; a bare super would forward all the arguments.
Raising it from a rescue clause captures the original error as one of the attributes:
def charge_customer
nil.object # Any code that raises an exception.
rescue => exception
raise CustomException.new(
333,
"A new exception occurred in the application",
exception
)
endWhoever rescues CustomException now gets the message, the code, and the original exception:
begin
charge_customer
rescue CustomException => e
e.message # => "A new exception occurred in the application"
e.code # => 333
e.cause # => #<NoMethodError: undefined method 'object' for nil>
endThat last line works without our details attribute: whenever you raise a new exception inside a rescue clause, Ruby stores the exception being handled in #cause automatically. The details attribute is still useful for context that isn’t an exception — a request ID, the payload that failed validation.
Two built-in methods surface this context when you log a rescued exception. #detailed_message appends the class name to the message:
e.detailed_message
# => "A new exception occurred in the application (CustomException)"And #full_message renders the complete report the default handler would print — message, class, and backtrace:
class PaymentError < StandardError; end
def charge
raise PaymentError, "card declined"
end
begin
charge
rescue => e
puts e.full_message
endcharge.rb:4:in 'Object#charge': card declined (PaymentError)
from charge.rb:8:in '<main>'
When the exception has a #cause, full_message prints both traces, the wrapping error first — so the NoMethodError behind a CustomException still shows up in your logs. If you report errors to AppSignal, you can attach the same kind of contextual data to error reports; the exception handling documentation shows how.
Building an Exception Hierarchy for Your App
Custom exceptions get more useful in groups. Take an image uploader in a Rails app that accepts only JPEGs between 100 kilobytes and 10 megabytes. Each rule violation gets its own error class, and the two size errors share a parent:
class ImageHandler
# Domain-specific errors
class ImageExtensionError < StandardError; end
class ImageDimensionError < StandardError; end
class ImageTooBigError < ImageDimensionError
def message
"Image is too big"
end
end
class ImageTooSmallError < ImageDimensionError
def message
"Image is too small"
end
end
def self.handle_upload(image)
raise ImageTooBigError if image.size > 10.megabytes
raise ImageTooSmallError if image.size < 100.kilobytes
raise ImageExtensionError unless %w[JPG JPEG].include?(image.extension)
# ... process the image
end
endNamespacing the errors inside ImageHandler keeps them discoverable — the class that raises them owns them. The message overrides give the size errors user-facing text, and ImageDimensionError exists purely as a grouping point: nothing raises it directly.
The payoff comes at rescue time. A controller can handle the whole dimension family with one clause while treating a suspicious file extension differently:
class ImageUploadController < ApplicationController
def upload
@image = params[:image]
ImageHandler.handle_upload(@image)
redirect_to :index, notice: "Image upload success!"
rescue ImageHandler::ImageDimensionError => e
render "edit", alert: "Error: #{e.message}"
rescue ImageHandler::ImageExtensionError
head :forbidden
end
endRescuing the parent class ImageDimensionError catches both ImageTooBigError and ImageTooSmallError, and each one’s message override supplies the right alert text. The extension error returns a 403 Forbidden instead — a non-JPEG upload is a security concern, not a user mistake. A domain error class is also the natural thing to re-raise as when you rescue a low-level failure, clean up, and pass it along — a pattern covered in ensure, retry, and reraise exceptions in Ruby.
Gems use the same structure at scale. The mongo-ruby-driver gem defines a Mongo::Error base class with a subclass per failure mode, so applications can rescue one specific error, a family, or everything the driver raises.
Custom exception classes pay off most in production: AppSignal’s Ruby error tracking groups incidents by exception class, so a PaymentError hierarchy turns a wall of RuntimeErrors into named failures you can route, mute, and alert on per class.
For the full production story — where these reports go and how to route them — see error reporting for Rails exceptions.
Common Errors When Raising Custom Exceptions
Two errors come up again and again while writing custom exception classes. Both messages here are verbatim Ruby 3.4 output.
exception class/object expected (TypeError)
class Payment; end
raise Payment, "card declined"payment.rb:3:in 'Kernel#raise': exception class/object expected (TypeError)
raise Payment, "card declined"
^^^^^^^^^^^^^^^^^^^^^^^^
from payment.rb:3:in '<main>'
Cause: raise accepts an Exception subclass, an exception instance, or a string (which becomes a RuntimeError). Anything else — a plain class like Payment here, an integer like raise 42 — fails with this TypeError before any of your error handling runs.
Fix: Make the class you’re raising inherit from StandardError:
class PaymentError < StandardError; enduninitialized constant (NameError)
begin
raise "the payment failed"
rescue StandardException => e
puts "rescued"
endcheckout.rb:3:in '<main>': uninitialized constant StandardException (NameError)
rescue StandardException => e
^^^^^^^^^^^^^^^^^
Did you mean? StandardError
checkout.rb:2:in '<main>': the payment failed (RuntimeError)
Cause: The rescue clause names an exception class that doesn’t exist — there is no StandardException in Ruby; the constant has never existed. Ruby resolves the constant in a rescue clause only when an exception is raised, so the typo hides until something goes wrong. The resulting NameError then replaces the error you meant to handle — the original RuntimeError appears after it as the cause.
The un-namespaced form of the same mistake: raise ImageTooBigError from outside the ImageHandler class fails with uninitialized constant ImageTooBigError (NameError).
Fix: Reference an exception class that exists, by its full constant path — StandardError, ImageHandler::ImageTooBigError.
Wrapping Up and Next Steps
A custom exception in Ruby is a one-line class: inherit from StandardError, raise it, rescue it by name. Your reward is error reports that name the real problem.
Happy coding!
P.S. If you’d like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!
Frequently asked questions
- How do you create a custom exception in Ruby?
- Define a class that inherits from StandardError, for example class PaymentError < StandardError; end. Raise it with raise PaymentError or raise PaymentError, followed by a message string, and rescue it by class name like any built-in Ruby exception.
- Should a custom Ruby exception inherit from StandardError or Exception?
- Inherit from StandardError. A bare rescue clause only catches StandardError and its descendants, so exceptions built on it behave the way calling code expects. Subclassing Exception directly means your error escapes normal rescue blocks and gets caught alongside signals and system-level failures.
- How do you add a default message to a custom exception in Ruby?
- Override initialize with a default message and pass it to super. If initialize never calls super with a message, Ruby falls back to the class name — raising PaymentError with no message then reports the string PaymentError instead of anything useful.
- What happens if you raise a class that does not inherit from Exception?
- Ruby raises TypeError with the message exception class/object expected. The raise keyword accepts an Exception subclass, an exception instance, or a string; anything else, like a plain class or an integer, fails with that TypeError before your error handling runs.
Published , Updated
Wondering what you can do next?
- Subscribe to our Ruby Magic newsletter and never miss an article again.
- Start monitoring your Ruby app with AppSignal.
- Share this article on social media

Brena Monteiro
Guest author Brena is a Tech Lead passionate about mentoring new developers and experienced in developing and leading high-performing teams. She has experience in building scalable APIs and integrations between cloud services, is enthusiastic about evolutionary architecture, and is an Extended Reality (XR) Apprentice.
All articles by Brena MonteiroBecome our next author!
AppSignal monitors your apps
AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!


