Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace raise with throw to handle context failure #133

Open
wants to merge 2 commits into
base: v4
Choose a base branch
from

Conversation

hedgesky
Copy link
Contributor

@hedgesky hedgesky commented Mar 25, 2017

Implements #126.

Thrown symbol name

I've decided to pick :early_return because we're going to support early success as well as failure.
At the earlier stage of implementing this I introduced an additional argument to throw:

throw :early_return, RunFailure.new(self)

But then I realized it's redundant: context itself knows is it successful or failed.

Aborting organized interactors chain execution on failure

Earlier we used #call! which raised an error to ensure this; now it's not true. To tackle this problem I've decided to check if context is failed on each step and throw :early_return if detect one.

So we have two issues here:

  • duplicating part of Context#fail! behavior here;
  • possible obstacles for permitting failures for certain interactors in a chain.

Docs

I postponed updating internal methods' documentation until we come to an agreement about proposed changes.

I'll be happy to discuss this!

P.S. integration specs pass without changes, thus external behavior didn't change. I think it's a big win 😄

@@ -8,7 +8,7 @@ module Interactor
# class MyOrganizer
# include Interactor::Organizer
#
# organizer InteractorOne, InteractorTwo
# organize InteractorOne, InteractorTwo
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

Copy link
Collaborator

@laserlemon laserlemon left a comment

Choose a reason for hiding this comment

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

I'm "requesting changes" on this PR mostly because I want us to think about the implementation for longer before I pull the trigger. Great work so far!

@@ -76,7 +76,8 @@ module InstanceMethods
# Returns nothing.
def call
self.class.organized.each do |interactor|
interactor.call!(context)
throw(:early_return) if context.failure?
Copy link
Collaborator

Choose a reason for hiding this comment

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

Duplicating context.fail! logic here doesn't feel right. I don't have a better suggestion yet, though. :)

@@ -123,7 +123,7 @@ def failure?
def fail!(context = {})
context.each { |key, value| modifiable[key.to_sym] = value }
@failure = true
raise Failure, self
throw :early_return
Copy link
Collaborator

Choose a reason for hiding this comment

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

I have to let this naming sit with me for a little bit.

context.rollback! if context.failure?
rescue
context.rollback!
raise
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd like to see if we could preserve how the primary functionality lived in run! rather than in run.

Copy link
Collaborator

Choose a reason for hiding this comment

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

In general, I need to stare at and think about this for a little while. I'm not as familiar with throw and catch as I am with raise and rescue, so we just need to think through the edge cases.

@@ -76,7 +76,8 @@ module InstanceMethods
# Returns nothing.
def call
self.class.organized.each do |interactor|
interactor.call!(context)
context.signal_early_return! if context.failure?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've extracted throwing symbol from Context#fail to a dedicated method so we can avoid logic duplication.

@hedgesky hedgesky force-pushed the replace_raise_with_throw branch from 661413b to 6c01d9f Compare April 25, 2017 22:10
@hedgesky
Copy link
Contributor Author

I've resolved conflicts on this branch.
How's your staring going? 😃

@n00ge
Copy link

n00ge commented Jun 20, 2018

I agree that these changes would be a great addition. Any updates on considering this?

@simonc
Copy link

simonc commented Sep 24, 2018

Hello there. I used a monkey-patched behavior that is very similar to this one. Being able to succeed early can be very useful (especially in background jobs for instance), I'd love to see this PR land on master.

Is there anything blocking its merge, is there anything we/I can do to help?

Thanks ❤️

@n00ge
Copy link

n00ge commented Apr 4, 2020

Bump: Been a while and checking back in here. Would still love to see this merged.

@n00ge
Copy link

n00ge commented Aug 31, 2020

Sorry, is there a reason this was closed? The PR above was never merged.

@gaffneyc
Copy link
Member

@hedgesky Sorry for the late ping on a very old pull request. I'm reviewing work and plans started by Steve before he moved on.

Do you happen to remember why this change was being made? All of the comments and documentation here focus on what the change is rather than the problem it was looking to solve.

@simonc
Copy link

simonc commented Dec 29, 2024

Hi @gaffneyc. IIRC it was to provide an "early success" capability without using weird exception handling for it.

@n00ge
Copy link

n00ge commented Dec 29, 2024

The reason why here is to use exceptions in exceptional cases rather than for control flow as used here. See below:

In Ruby, exceptions are a way to handle unexpected situations or errors that occur during the execution of a program. They are objects that can be created, raised, and rescued in your code. Using exceptions "exceptionally" means that you should only employ them for truly unexpected situations, rather than using them as a regular part of your program’s control flow.

What are Exceptions in Ruby?
Exceptions in Ruby are instances of classes that descend from the Exception class. The most common way to handle exceptions in Ruby is through the begin, rescue, and end block, which allows you to define a block of code that might raise an exception and specify how to handle those exceptions if they occur.

Why Use Exceptions Correctly?
Clarity and Maintainability: Using exceptions for control flow can make your code more difficult to understand. It obscures the logic of your program and makes it harder for others (or you at a later date) to follow the intended flow of execution.
Performance: Raising and rescuing exceptions is generally slower than using conditional logic or other control flow mechanisms, such as loops or conditionals (e.g., if, else). If exceptions are used frequently in regular control flow, it can degrade the performance of your application.
Error Handling: Exceptions should be reserved for handling error conditions that the code cannot predict or handle through normal logic. For example, trying to open a file that does not exist is an appropriate use of exceptions because it is not something that can always be anticipated or prevented by the program logic.
Conventional Usage: In most programming languages, including Ruby, exceptions are conventionally used to handle errors and unexpected events. Misusing them can lead to confusion and bugs, especially if other programmers are assuming conventional use.

Best Practices for Using Exceptions
Use exceptions to handle unexpected errors, not for business logic: Ensure that the business logic of your application is handled using proper control structures like loops and conditionals.
Be specific in your rescuing: Rescue specific exception types rather than using a general rescue clause. This ensures that only the exceptions you expect are handled, and unexpected ones do not go unnoticed.
Always clean up resources: Use the ensure block to release resources, such as file handles or network connections, ensuring that they are freed even if an exception occurs.
Log exceptions appropriately: Make sure that exceptions are logged so that they can be reviewed and addressed. This is especially important for debugging and maintaining the software.

By understanding the role of exceptions and using them appropriately, you can write cleaner, more efficient, and more reliable Ruby code.

@hedgesky
Copy link
Contributor Author

@gaffneyc, here's the original reason for this PR: #120.

In nutshell, if the developer rescues exceptions within their interactors, they would also likely (yet accidentally) rescue Interactor::Failure thrown by context.fail!. Using throw instead of exceptions avoids this issue.

@gaffneyc
Copy link
Member

@hedgesky Thank you for finding that! The rationale makes sense to avoid potential user error when catching exceptions.

I'm not as convinced yet about the need for triggering an early success which skips later Interactors as I feel that will make it harder to fully map an Organizer's behavior. My personal take, though there seems to be a want for it, is that an organizer is expected to call all Interactors it organizes unless there is an error.

I'm going to reopen this to keep track of it for future 4.0 release. I appreciate that work that's gone into this. It is also early enough in figuring out what the next version is going to look like that I'm not decided on a direction yet.

@gaffneyc gaffneyc reopened this Dec 29, 2024
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.

5 participants