Blog Implementation Guides 7 min read

Ruby on Rails image_tag Fallback for Missing Attachments

Handle missing ActiveStorage attachments and broken image URLs in Rails using model methods, image_tag helpers, and fallback.pics placeholder URLs for user content.

rails image placeholderactive storage fallbackrails image_tagruby on railsplaceholder image
Ruby on Rails image_tag Fallback for Missing Attachments

Rails applications using ActiveStorage face a specific challenge: has_one_attached and has_many_attached associations return proxy objects even when no file is attached — you must call .attached? before generating a URL, or you will hit an error.

The right pattern is a model method that checks attachment presence and returns either the ActiveStorage URL or a generated fallback, keeping ERB templates clean and the fallback behavior consistent.

ActiveStorage behavior

How ActiveStorage handles missing attachments

ActiveStorage associations return a proxy object regardless of whether a file is attached. Calling url_for on an unattached file raises an ActiveStorage::FileNotFoundError or a similar error depending on the Rails version.

Always call .attached? before generating URLs for ActiveStorage attachments. Do not rely on nil checks — the association object is not nil even when no file is attached.

Files that were attached but later deleted from the storage backend (S3, GCS, or disk) will have valid metadata in the database but return a 404 or 403 from the storage service. The .attached? check returns true in this case.

Model method

Add an image_url model method

Encapsulate the fallback logic in the model. The method checks attachment presence and returns the ActiveStorage URL or a generated fallback URL.

Accept size parameters so the method produces fallback URLs that match different slot sizes across the app.

Implementation text
# app/models/product.rb
class Product < ApplicationRecord
  has_one_attached :image

  def image_url(width: 400, height: 400)
    if image.attached?
      Rails.application.routes.url_helpers.url_for(image)
    else
      "https://fallback.pics/api/v1/#{width}x#{height}?text=#{CGI.escape(name)}"
    end
  end
end

ERB template

Use image_tag with the model method

Rails' image_tag helper accepts any URL string. Pass the model method result and add an onerror attribute for runtime CDN failures.

The onerror attribute in image_tag is passed as part of the html_options hash.

Implementation text
<%# app/views/products/_card.html.erb %>
<%= image_tag(
  product.image_url(width: 400, height: 400),
  alt: product.name,
  width: 400,
  height: 400,
  loading: "lazy",
  onerror: "this.onerror=null; this.src='https://fallback.pics/api/v1/400x400?text=No+Image'"
) %>

User avatars

User avatar fallback in Rails with initials

User avatar slots are a common ActiveStorage use case. Use initials from the user's name in the fallback URL to create a more informative placeholder than a blank square.

Implementation text
# app/models/user.rb
def avatar_url(size: 64)
  if avatar.attached?
    Rails.application.routes.url_helpers.url_for(
      avatar.variant(resize_to_fill: [size, size])
    )
  else
    initials = name.split.map { |w| w[0] }.join.upcase.first(2)
    "https://fallback.pics/api/v1/avatar/#{size}?text=#{CGI.escape(initials)}"
  end
end

OG image

Resolve og:image in Rails controllers

Set an og_image instance variable in the controller before rendering. Use the model method with OG-appropriate dimensions.

Implementation text
# app/controllers/products_controller.rb
def show
  @product = Product.find_by!(slug: params[:slug])
  @og_image = @product.image_url(width: 1200, height: 630)
end

<%# In layout %>
<%= tag.meta property: 'og:image', content: @og_image %>
<%= tag.meta property: 'og:image:width', content: '1200' %>
<%= tag.meta property: 'og:image:height', content: '630' %>

Variant fallback

Handle ActiveStorage variant processing failures

ActiveStorage variants are processed on first access. If processing fails — for example, because the original file is corrupt — the variant URL returns a 500 error.

Wrap variant generation in a begin/rescue block and return the fallback URL if it raises. Log the failure so you can investigate corrupt source files separately.

Implementation text
def thumbnail_url(width: 200, height: 200)
  return "https://fallback.pics/api/v1/#{width}x#{height}?text=No+Image" unless image.attached?

  begin
    Rails.application.routes.url_helpers.url_for(
      image.variant(resize_to_fill: [width, height]).processed
    )
  rescue => e
    Rails.logger.warn("Image variant failed for Product##{id}: #{e.message}")
    "https://fallback.pics/api/v1/#{width}x#{height}?text=Image+Error"
  end
end

Key takeaways

What to standardize before shipping

  • Always call .attached? before generating ActiveStorage URLs — the association is not nil even without a file.
  • Encapsulate fallback URL logic in a model method so templates, serializers, and API responses all behave consistently.
  • Add an onerror attribute in image_tag html_options for files that pass .attached? but return a CDN error.
  • Wrap .variant(...).processed in begin/rescue to handle corrupt source files gracefully.
  • Use CGI.escape() when embedding model attributes in fallback.pics text parameters.

Production fallback layer

Use fallback.pics anywhere an image URL is accepted.

Start with one deterministic URL and standardize fallback behavior across your design system.

Read the docs