Skip to content

Allow #step to be given a name, surfaced to #on_failure#42

Open
timriley wants to merge 1 commit into
mainfrom
optional-step-names
Open

Allow #step to be given a name, surfaced to #on_failure#42
timriley wants to merge 1 commit into
mainfrom
optional-step-names

Conversation

@timriley
Copy link
Copy Markdown
Member

@timriley timriley commented May 15, 2026

Allow steps to be given an optional name, so that the on_failure hook can differentiate between steps.

Now you can define steps like this:

class MyOperation < Dry::Operation
  def call(input)
    attrs = step :validate, validate(input)
    user = step :persist, persist(attrs)
    step :notify, notify(user)

    # Unnamed steps still work as always:
    #
    # attrs = step validate(input)
    # user = step persist(attrs)
    # step notify(user)
    
    user
  end
end

With this done, you can use a new form on on_failure, which now accepts step_name: and method_name: as optional keyword arguments:

def on_failure(failure, step_name:)
  case step_name
  when :validate
    # ...
  when :persist
    # ...
  else
    # ...
  end


  # or even something so simple as:
  log(failure, step_name)
end

Knowing the step name inside on_failure makes it much easier to add step-specific failure handling, like we see above. You can supply one or both of method_name: and step_name: to this method — we do params introspection behind the scenes to figure out what to pass.

Existing on_failure params signatures work exactly as before:

on_failure(failure)
on_failure(failure, method_name)

I expect this may be just the first step (🥁) in use of step names. Down the track we might want to consider their usage in other areas. For example, a nicer DSL for per-step failure handlers, standard failure structures "tagged" with the step name prepended, per-step "recovery" flows, instrumentation hooks, etc.

For starters, though, we keep this simple and focused on improving the facilities we already have.

To this end, while we're here, enhance the validation plugins so that it provides a :validation step name when validation fails.

Implementation notes

  • We pass the step name through to on_failure via the throw payload, which now becomes a StepFailure data class, holding both the failure and the step name.
  • Our steps block API (in part an internal concern, but also a piece of lower-level public API) now invokes on_failure when a failure is encountered. This closes a gap in its earlier functionality, and allows us to continue using it from within ClassContext::StepsMethodPrepender.
  • Calling on_failure has now become more complex with our params introspection, so that logic is now moved to a separate ClassContext::FailureHookDispatcher module.

@timriley timriley force-pushed the optional-step-names branch from 03b9a2c to eeeb902 Compare May 15, 2026 22:54
Copy link
Copy Markdown
Member

@waiting-for-dev waiting-for-dev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!!!!

# hook, supporting several signatures:
#
# def on_failure(failure)
# def on_failure(failure, method_name)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could find a more descriptive name instead of method_name. I thought about operation_name but it'd also be confusing given we define the class as a Dry::Operation 🤔

Comment thread lib/dry/operation.rb
# attrs = step validate(input)
# user = step persist(attrs)
# step notify(user)
# attrs = step :validate, validate(input)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think keeping the value as the first positional parameter would read better?

I’m thinking about something like:

step validate(input), as: :validate

Not a strong opinion, but it could also make refactoring easier when we want to provide a name in a second thought, since appending something to a line is always easier than inserting something in the middle for most IDEs 🙂

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reads more naturally. Is it possible to use reflection to infer the method's name and omit the symbol altogether, making it optional?

step validate(input)           # on_failure gets :validate as step_name
step persist(input), as: :save # on_failure gets :save as step_name

@alassek
Copy link
Copy Markdown
Contributor

alassek commented May 18, 2026

@timriley Down the track we might want to consider their usage in other areas. For example, a nicer DSL for per-step failure handlers, standard failure structures "tagged" with the step name prepended

I have been using my own helper method for this called either and I've landed on three useful forms:

functional mapping, because sometimes I want to transform the errors somehow. For instance, a Dry::Types error result returns multiple values and I want to splat them into the Failure tuple.

attrs = either validate(input), ->(*err) { Failure[:validation, *err] }

Sometime's I'm doing just a sanity check, and I don't need to lazy-process the error result, so being able to eagerly pass a Failure is useful:

either T::Callable.try(discriminator), Failure[:type, discriminator]

Finally, the vast majority of cases I just want to wrap a Failure in a tuple and assign a name. I do it this way:

attrs = either validate(input), error: :validation
# produces: Failure[:validation, Dry::Schema::Result]

This is most commonly done to mark HTTP errors as Failure[:http, Response]

Doing this inline, I've never felt the need for something like on_failure at all, except in the case of standardized logging for background jobs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants