Question

How to Return a 404 Response in Ruby on Rails

rubyruby-on-railshttphttp-status-code-404

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:

  • render to show a template or file
  • head to send only status code and headers
  • exceptions such as ActiveRecord::RecordNotFound that 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.

Take Quiz

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.html file
  • 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

  1. Rails routes the request to ProductsController#show.

  2. params[:id] contains "999".

  3. Product.find_by(id: params[:id]) runs a database query.

  4. No matching product is found, so product becomes nil.

  5. The condition product.nil? is true.

  6. Rails executes:

    render file: Rails.public_path.join("404.html"), status: :not_found, layout: false
    
  7. Rails sends:

    • the contents of public/404.html
    • HTTP status code 404
  8. return stops the action so the final render plain: product.name line does not run.

If the product exists

If Product.find_by(id: params[:id]) returns a record instead of nil:

  • the if block 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.

Take Quiz

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 3xx response 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, user is nil
  • user.name will 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

ApproachWhat it doesBest forNotes
head :not_foundSends only a 404 status, no page bodyAPIs, lightweight responsesVery simple
render ..., status: :not_foundSends content plus 404 statusHTML pages, JSON errors, text responsesMost flexible
raise ActiveRecord::RecordNotFoundSignals that a record is missingRecord lookups with findRails can handle this automatically
redirect_toSends a redirect responseMoving users to another pageNot a true 404

find vs find_by

MethodIf record existsIf record does not existTypical use
find(id)Returns recordRaises ActiveRecord::RecordNotFoundWhen missing record should become 404/error flow
find_by(id: id)Returns recordReturns nilWhen you want to handle absence manually

render vs head

MethodIncludes response body?Can set status?Common use
renderYesYesHTML, JSON, plain text, files
headNoYesStatus-only responses
Take Quiz

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_found for a real 404 response
  • Use head :not_found when no response body is needed
  • Use render when you want to show a page or return JSON/text
  • Use find when a missing record should naturally become a 404 path
  • Use find_by when you want to check nil yourself
  • Do not use redirect_to to 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?

  • find raises ActiveRecord::RecordNotFound if nothing is found
  • find_by returns nil

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.

Take Quiz

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 ArticlesController with a show action.
  • 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.
Take Quiz

Keep learning

Related questions

View more