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 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.
# 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.
<%# 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.
# 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.
# 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.
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.