Question
In Ruby on Rails, how can I return a 404 "Not Found" response to the client?
In PHP, this might be done by manually sending an HTTP header such as:
header("HTTP/1.0 404 Not Found");
What is the Rails way to do the same thing, especially when I want to show a not found page or indicate that a resource does not exist?
Short Answer
By the end of this page, you will understand how Rails sends HTTP 404 responses, how to render a custom not found page, when to raise an exception versus rendering directly, and which patterns are commonly used in real Rails applications.
Concept
In web applications, a 404 Not Found response means the server was reached successfully, but the requested resource does not exist.
In Rails, you usually do not manually write raw HTTP headers the way you might in PHP. Instead, Rails provides higher-level methods for building a response:
renderto show a template or fileheadto send only status code and headers- exceptions such as
ActiveRecord::RecordNotFoundthat Rails can convert into a 404 response
This matters because Rails follows the conventions of the HTTP request/response cycle. Rather than thinking "send a header string," think in terms of:
- What content should the user see?
- What HTTP status should be returned?
A 404 is important for both users and machines:
- Users see that the page does not exist
- Browsers and API clients understand the response correctly
- Search engines know the page is missing instead of thinking it loaded successfully
A common beginner mistake is to render a page that says "Not Found" but forget to set the status to 404. In that case, the browser receives a 200 OK, which is misleading.
In Rails, common ways to return a 404 include:
head :not_found
or:
render file: Rails.public_path.join("404.html"), status: :not_found
or by raising:
raise ActiveRecord::RecordNotFound
Each option is useful in different situations.
Mental Model
Think of a Rails controller action like a receptionist at a building.
- If the visitor asks for a person who works there, the receptionist sends them to the right room.
- If the person does not exist, the receptionist says, "That person is not here," and marks the request as not found.
In PHP, you might manually write the receptionist's note yourself. In Rails, you usually hand Rails a structured instruction such as:
- "Send no body, just say not found"
- "Show this 404 page and mark it as not found"
- "This record doesn't exist; treat it as not found"
So instead of manually constructing the raw HTTP message, you tell Rails the meaning of the response, and Rails builds the correct HTTP output for you.
Syntax and Examples
Core syntax
1. Return only a 404 status
head :not_found
Use this when you do not need to render a page body, such as in a small API endpoint.
2. Render a page with 404 status
render file: Rails.public_path.join("404.html"), status: :not_found, layout: false
Use this when you want to show a static error page.
3. Render custom text or JSON with 404 status
render plain: "Not Found", status: :not_found
render json: { error: "Not Found" }, status: :not_found
This is useful for APIs or simple responses.
Beginner-friendly example
Imagine you are looking up an article by ID:
class ArticlesController < ApplicationController
def show
article = Article.find_by(id: params[:id])
if article.nil?
render file: Rails.public_path.join("404.html"), status: :not_found, layout: false
return
end
render :show
end
end
What this does
Article.find_by(id: params[:id])tries to find the article- If it returns
nil, the article does not exist - Rails renders the
public/404.htmlfile - The HTTP status is correctly set to
404 Not Found
Using exceptions
Rails developers often write this more idiomatically with find:
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
end
end
If the record does not exist, Article.find raises ActiveRecord::RecordNotFound. Rails can handle that and respond with a 404 automatically, depending on environment and configuration.
This is often cleaner than manually checking for nil.
Step by Step Execution
Consider this controller action:
class ProductsController < ApplicationController
def show
product = Product.find_by(id: params[:id])
if product.nil?
render file: Rails.public_path.join("404.html"), status: :not_found, layout: false
return
end
render plain: product.name
end
end
Now imagine the request is:
GET /products/999
and product 999 does not exist.
Step by step
-
Rails routes the request to
ProductsController#show. -
params[:id]contains"999". -
Product.find_by(id: params[:id])runs a database query. -
No matching product is found, so
productbecomesnil. -
The condition
product.nil?istrue. -
Rails executes:
render file: Rails.public_path.join("404.html"), status: :not_found, layout: false -
Rails sends:
- the contents of
public/404.html - HTTP status code
404
- the contents of
-
returnstops the action so the finalrender plain: product.nameline does not run.
If the product exists
If Product.find_by(id: params[:id]) returns a record instead of nil:
- the
ifblock is skipped - Rails runs
render plain: product.name - the response status is the default
200 OK
This is the key idea: the body and the HTTP status are separate parts of the response, and Rails lets you control both clearly.
Real World Use Cases
404 responses are used in many practical situations:
Missing database records
A user visits:
/articles/123
but article 123 has been deleted.
Hidden or unavailable content
Sometimes an app intentionally returns 404 instead of revealing whether a resource exists, for example:
- private documents
- unpublished posts
- disabled user profiles
API endpoints
An API may return:
{ "error": "User not found" }
with status 404 when a requested user ID does not exist.
Invalid nested routes
Example:
/users/5/orders/999
If order 999 does not belong to user 5, the app may return 404.
Old or removed pages
When content has been removed and no replacement exists, 404 is the correct status.
This helps search engines and clients understand the page is unavailable.
Real Codebase Usage
In real Rails projects, developers usually use a few common patterns.
1. Let find raise ActiveRecord::RecordNotFound
def show
@article = Article.find(params[:id])
end
This is common because it is short and expressive. Missing records are treated as exceptional cases.
2. Rescue missing-record exceptions in one place
Many apps handle 404 errors centrally in ApplicationController:
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
private
def render_not_found
render file: Rails.public_path.join("404.html"), status: :not_found, layout: false
end
end
This avoids repeating the same code in many controllers.
3. Use guard clauses
def show
@article = Article.find_by(id: params[:id])
return render(file: Rails.public_path.join("404.html"), status: :not_found, layout: false) if @article.nil?
render :show
end
Guard clauses keep controller actions short and readable.
4. Return different formats
Real apps often respond differently for browser and API requests:
def render_not_found
respond_to do |format|
format.html { render file: Rails.public_path.join("404.html"), status: :not_found, layout: false }
format.json { render json: { error: "Not Found" }, status: :not_found }
end
end
5. Validation and authorization boundaries
Sometimes code returns 404 when a record is inaccessible in the current scope:
def show
@project = current_user.projects.find(params[:id])
end
If the project is not owned by the current user, find raises RecordNotFound, and the app can return 404.
This pattern is very common in multi-user applications.
Common Mistakes
1. Showing a 404 message with a 200 status
Broken example:
render plain: "404 Not Found"
Problem:
- The text says 404
- The actual HTTP status is still
200 OK
Fix:
render plain: "404 Not Found", status: :not_found
2. Using redirect_to for a 404
Broken idea:
redirect_to "/404"
Problem:
- A redirect sends a
3xxresponse first, not a 404 - It changes the URL instead of reporting that the requested resource was missing
Use render or head instead.
3. Forgetting to stop execution after rendering
Broken example:
def show
if something_missing?
render file: Rails.public_path.join("404.html"), status: :not_found, layout: false
end
render :show
end
Problem:
- Rails may try to render twice
- This can cause a
DoubleRenderError
Fix:
def show
if something_missing?
render file: Rails.public_path.join("404.html"), status: :not_found, layout: false
return
end
render :show
end
4. Using find_by without checking for nil
Broken example:
user = User.find_by(id: params[:id])
render plain: user.name
Problem:
- If no user is found,
userisnil user.namewill fail
Fix:
user = User.find_by(id: params[:id])
return head :not_found if user.nil?
render plain: user.name
5. Confusing 404 with 500
A missing record is usually a 404, not a 500.
- 404 = resource does not exist
- 500 = server crashed or encountered an unexpected error
Returning the correct status makes debugging and client handling much clearer.
Comparisons
| Approach | What it does | Best for | Notes |
|---|---|---|---|
head :not_found | Sends only a 404 status, no page body | APIs, lightweight responses | Very simple |
render ..., status: :not_found | Sends content plus 404 status | HTML pages, JSON errors, text responses | Most flexible |
raise ActiveRecord::RecordNotFound | Signals that a record is missing | Record lookups with find | Rails can handle this automatically |
redirect_to | Sends a redirect response | Moving users to another page | Not a true 404 |
find vs find_by
| Method | If record exists | If record does not exist | Typical use |
|---|---|---|---|
find(id) | Returns record | Raises ActiveRecord::RecordNotFound | When missing record should become 404/error flow |
find_by(id: id) | Returns record | Returns nil | When you want to handle absence manually |
render vs head
| Method | Includes response body? | Can set status? | Common use |
|---|---|---|---|
render | Yes | Yes | HTML, JSON, plain text, files |
head | No | Yes | Status-only responses |
Cheat Sheet
# Status only
head :not_found
# Render static 404 page
render file: Rails.public_path.join("404.html"), status: :not_found, layout: false
# Render text with 404
render plain: "Not Found", status: :not_found
# Render JSON with 404
render json: { error: "Not Found" }, status: :not_found
# Raise a missing-record exception
raise ActiveRecord::RecordNotFound
# Common Active Record lookup
@post = Post.find(params[:id]) # raises if missing
@post = Post.find_by(id: params[:id]) # returns nil if missing
Quick rules
- Use
status: :not_foundfor a real 404 response - Use
head :not_foundwhen no response body is needed - Use
renderwhen you want to show a page or return JSON/text - Use
findwhen a missing record should naturally become a 404 path - Use
find_bywhen you want to checknilyourself - Do not use
redirect_toto fake a 404 - After
render, stop execution if more code would continue
Symbol used for 404
:not_found
Equivalent HTTP code:
404
FAQ
How do I return a 404 in Rails?
Use one of these common approaches:
head :not_found
or:
render file: Rails.public_path.join("404.html"), status: :not_found, layout: false
How do I show the Rails 404 page?
Render the static file from public/404.html with status :not_found:
render file: Rails.public_path.join("404.html"), status: :not_found, layout: false
Is redirect_to the right way to send a 404?
No. redirect_to sends a redirect status like 302, not a 404. Use render or head instead.
What is the difference between find and find_by in Rails?
findraisesActiveRecord::RecordNotFoundif nothing is foundfind_byreturnsnil
Use find when missing records should be treated as not found automatically.
Can Rails automatically return 404 for missing records?
Yes. If you use Model.find(params[:id]) and the record does not exist, Rails raises ActiveRecord::RecordNotFound, which can be handled as a 404.
How do I return 404 in a Rails API?
A common pattern is:
render json: { error: "Not Found" }, status: :not_found
Why should I set the HTTP status instead of just showing a message?
Because clients, browsers, crawlers, and APIs rely on the actual HTTP status code. A page that says "Not Found" but returns 200 OK is misleading.
Mini Project
Description
Build a small Rails controller that serves article pages and correctly returns a 404 response when an article does not exist. This demonstrates the difference between a successful response and a proper not found response, which is a common requirement in real applications.
Goal
Create a controller action that finds an article by ID and returns either the article content or a proper 404 response.
Requirements
- Create an
ArticlesControllerwith ashowaction. - Look up an article using the ID from
params[:id]. - If the article does not exist, return a 404 response.
- If the article exists, show its title.
- Use Rails conventions rather than manually setting raw HTTP headers.
Keep learning
Related questions
Add a Default Value to an Existing Column in a Rails Migration
Learn how to add or change a default value on an existing database column using Rails Active Record migrations safely.
Calling a Class Method from an Instance in Ruby
Learn how to call a class method from an instance in Ruby using self.class, with examples, pitfalls, and practical usage patterns.
Calling an Overridden Monkey-Patched Method in Ruby
Learn how to call the original method when monkey patching in Ruby, including alias_method patterns, examples, pitfalls, and practical usage.