rescue_from: this app's one line, from a bad slug to a custom page
Where this app raises the exception
Course and lesson pages in this app are found by slug, not by numeric id. The query objects behind them:
# app/queries/catalog/find_course_by_slug_query.rb
Result.new(course: ::Catalog::Course.active.find_by!(slug: slug))
# app/queries/catalog/find_lesson_by_slug_query.rb
lesson = course.lessons.find_by(slug: slug)
return Result.new(lesson: lesson, renamed: false) if lesson
Result.new(lesson: course.lessons.where("? = ANY(previous_slugs)", slug).take!, renamed: true)
find_by!, the bang version, raises ActiveRecord::RecordNotFound when no row matches: Catalog::FindCourseBySlugQuery.call(slug: "does-not-exist") raises ActiveRecord::RecordNotFound: Couldn't find Catalog::Course .... The lesson query tries the current slug first, then the lesson's earlier slugs, so an old link to a renamed lesson can still be redirected. Only when neither matches does take! raise the same exception. A mistyped URL, an old bookmark to a removed course or a slug that never existed all end up as this one exception class.
The one line that catches it
app/controllers/application_controller.rb has one rescue_from call, and every controller in this app inherits from it:
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
end
One line. Every action in every controller, from Catalog::CoursesController to Enrollment::LessonCompletionsController and Public::ProfilesController, gets the same behavior when it raises ActiveRecord::RecordNotFound, without any of them writing its own begin/rescue.
render_not_found, as written
with: :render_not_found names a private method, also in ApplicationController:
def render_not_found
respond_to do |format|
format.html do
@not_found = not_found_context
@suggested_courses = Public::NotFoundSuggestionsQuery.call.courses
render "errors/not_found", status: :not_found
end
format.any { head :not_found }
end
end
def not_found_context
if params[:username].present?
{ kind: :profile, name: params[:username] }
elsif params[:course_slug].present? && params[:slug].present?
{ kind: :lesson, name: params[:slug].to_s.tr("-", " ") }
elsif params[:slug].present? || params[:course_slug].present?
{ kind: :course, name: (params[:slug] || params[:course_slug]).to_s.tr("-", " ") }
else
{ kind: :page, name: nil }
end
end
not_found_context reads whichever params the failed request had and decides what went missing. It can be a profile (params[:username]), a lesson (both course_slug and slug), a course (just one of them) or a generic page. render_not_found branches on format. An HTML request gets the styled errors/not_found view, with that context and a list of suggested courses. Anything else, such as an API client asking for JSON, gets a bare head :not_found: a 404 status with no body.
The view it renders
app/views/errors/not_found.html.erb uses @not_found[:kind] to pick an icon: a person for a missing profile, a book for anything else. It also looks up the heading, body and hint text through t("errors.not_found.heading.#{kind}", name: name) and its siblings, one i18n key per kind. Below that, @suggested_courses, when there are any, renders as a grid of course cards with the same catalog_course_path links and level badges used across the app.
What one rescue_from line replaces
Without it, every action that can raise ActiveRecord::RecordNotFound would need its own begin/rescue around the same steps: decide the kind, load suggestions, render the view, set the status. rescue_from, registered once on ApplicationController, keeps that logic in one place instead of copied into every controller that looks something up by slug.
What this course covers
Seven lessons, built on this app's code and Rails' installed source. This lesson covers this app's use, start to finish. Lesson 2 covers the two forms rescue_from accepts, with: and a block. Lesson 3 covers what happens with more than one rescue_from: declaration order and inheritance, tested against Rails' matching algorithm. Lesson 4 covers is_a? and the family of ActiveRecord exceptions this app never had to name one by one. Lesson 5 covers Rails' default mapping from exceptions to status codes, and what a custom rescue_from adds. Lesson 6 covers the two ways an exception can still get through: no handler matches, or the handler itself fails. Lesson 7 ends with a small simulator of the matching algorithm.
Keep going with lesson 2
with: or a block: the two forms rescue_from accepts
Compares the with: and block forms, which share one slot, and shows how arity controls the exception argument.
A free account also gives you the quizzes, your progress and notes, and the discussion.