19

We use exceptions to let the consumer of the code handle unexpected behaviour in a useful way. Usually exceptions are built around "what happened" scenario - like FileNotFound (we were unable to find file you specified) or ZeroDivisionError (we were unable to perform 1/0 operation).

What if there is a possibility to specify the expected behaviour of the consumer?

For example, imagine we have fetch resource, which performs HTTP request and returns retrieved data. And instead of errors like ServiceTemporaryUnavailable or RateLimitExceeded we would just raise a RetryableError suggesting the consumer that it should just retry the request and do not care about specific failure. So, we are basically suggesting an action to the caller - the "what to do".

We do not do this often because we don't know all the usecases of consumers. But imagine that is some specific component we do know the best course of actions for a caller - so should we then make use of "what to do" approach?

  • 3
    Doesn't HTTP already do this? 503 is a temporary failure to reply, so the requester should retry, 404 is a fundamental absence, so it makes no sense to retry, 301 means "moved permanently", so you should retry, but with a different address, etc. – Kilian Foth Dec 22 '15 at 16:01
  • 7
    In many cases, if we really do know "what to do", we can just make the computer do it automatically and the user doesn't even have to know anything went wrong. I assume whenever my browser receives a 301 it simply goes to the new address without asking me. – Ixrec Dec 22 '15 at 16:02
  • @Ixrec - had the same idea too. however, the consumer may not want to wait for another request and ignore the item or fail completely. – Roman Bodnarchuk Dec 22 '15 at 16:06
  • 1
    @RomanBodnarchuk: I disagree. It's like saying a person shouldn't need to know Chinese in order to speak Chinese. HTTP is a protocol, and both the client and the server are expected to know it and follow it. That's how a protocol works. If only one side knows and abides by it, then you can't communicate. – Chris Pratt Dec 22 '15 at 19:37
  • 1
    This honestly sounds like you're trying to replace your exceptions with a catch block. Thats why we moved to exceptions - no more if( ! function() ) handle_something();, but being able to handle the error somewhere you actually know the calling context - i.e. tell a client to call a sys admin if your server failed or reload automatically if the connection dropped, but alert you in case the caller is another microservice. Let the catch blocks handle the catching. – Sebb Dec 22 '15 at 23:06
47

But imagine that is some specific component we do know the best course of actions for a caller.

This almost always fails for at least one of your callers, for which this behaviour is incredibly irritating. Don't assume you know best. Tell your users what's happening, not what you assume they should do about it. In many cases it's already clear what a sane course of action should be (and, if it's not, make a suggestion in your user manual).

For example, even the exceptions given in your question demonstrate your broken assumption: a ServiceTemporaryUnavailable equates to "try again later", and RateLimitExceeded equates to "woah there chill out, maybe adjust your timer parameters, and try again in a few minutes". But the user may as well want to raise some sort of alarm on ServiceTemporaryUnavailable (which indicates a server problem), and not for RateLimitExceeded (which doesn't).

Give them the choice.

  • 4
    I agree. The server should only convey the information properly. The documentation on the other hand should clearly outline the proper course of action in such cases. – Neil Dec 22 '15 at 16:13
  • 1
    One tiny flaw in this is that to some hackers, certain exceptions can tell them quite a lot about what your code is doing and they can use it to figure out ways to exploit it. – Pharap Dec 23 '15 at 2:46
  • 3
    @Pharap If your hackers have access to the exception itself instead of an error message, you're already lost. – corsiKa Dec 23 '15 at 3:02
  • 2
    I like this answer, but it's missing what I consider a requirement of exceptions... if you knew how to recover, it wouldn't be an exception! An exception should only be when for when you can't do something about it: invalid input, invalid state, invalid security - you can't fix those programatically. – corsiKa Dec 23 '15 at 3:04
  • 1
    Agreed. If you insist on indicating whether a retry is possible, you could always just make the relevant concrete exceptions inherit from RetryableError. – sapi Dec 23 '15 at 4:57
18

Warning! C++ programmer coming in here with possibly-different ideas of how exception-handling should be done trying to answer a question which is certainly about another language!

Given this idea:

For example, imagine we have fetch resource, which performs HTTP request and returns retrieved data. And instead of errors like ServiceTemporaryUnavailable or RateLimitExceeded we would just raise a RetryableError suggesting the consumer that it should just retry the request and do not care about specific failure.

... one thing I would suggest is that you might be mixing up concerns of reporting an error with courses of action to respond to it in a way that might degrade the generality of your code or require a lot of "translation points" for exceptions.

For example, if I model a transaction involving loading a file, it might fail for a number of reasons. Perhaps loading the file involves loading a plugin which does not exist on the user's machine. Perhaps the file is simply corrupt and we encountered an error in parsing it.

No matter what happens, let's say the course of action is to report what happened to the user and prompt him about what he wants to do about it ("retry, load another file, cancel").

Thrower vs. Catcher

That course of action applies regardless of what kind of error we encountered in this case. It's not embedded into the general idea of a parsing error, it's not embedded into the general idea of failing to load a plugin. It's embedded into the idea of encountering such errors during the precise context of loading a file (the combination of loading a file and failing). So typically I see it, crudely speaking, as the catcher's responsibility to determine the course of action in response to a thrown exception (ex: prompting the user with options), not the thrower's.

Put another way, the sites that throw exceptions typically lack this kind of contextual information, especially if the functions that throw are generally applicable. Even in a totally degeneralized context when they have this information, you end up cornering yourself in terms of recovery behavior by embedding it into the throw site. The sites that catch are the ones that generally have the most amount of information available to determine a course of action, and give you one central place to modify if that course of action should ever change for that given transaction.

When you start trying to throw exceptions no longer reporting what's wrong but trying to determine what to do, that might degrade the generality and flexibility of your code. A parsing error shouldn't always lead to this kind of prompt, it varies by the context in which such an exception is thrown (the transaction under which it was thrown).

The Blind Thrower

Just in general, a lot of the design of exception-handling often revolves around the idea of a blind thrower. It doesn't know how the exception is going to be caught, or where. The same applies for even older forms of error recovery using manual error propagation. Sites that encounter errors do not include a user course of action, they only embed the minimal information to report what kind of error was encountered.

Inverted Responsibilities and Generalizing the Catcher

On thinking about this more carefully, I was trying to imagine the kind of codebase where this might become a temptation. My imagination (possibly wrong) is that your team is still playing the role of the "consumer" here and implementing most of the calling code as well. Perhaps you have a lot of disparate transactions (a lot of try blocks) that can all run into the same sets of errors, and all should, from a design perspective, lead to a uniform course of recovery action.

Taking into account the wise advice from Lightness Races in Orbit's fine answer (which I think is really coming from an advanced library-oriented mindset), you might still be tempted to throw "what to do" exceptions, only closer to the transaction recovery site.

It might be possible to find an intermediary, common transaction-handling site out of this here which actually centralizes the "what to do" concerns but still within the context of catching.

enter image description here

This would only apply if you can design some kind of general function which all of these outer transactions use (ex: a function that inputs another function to call or an abstract transaction base class with overridable behavior modeling this intermediary transaction site that does the sophisticated catching).

Yet that one could be responsible for centralizing the user course of action in response to a variety of possible errors, and still within the context of catching rather than throwing. Simple example (Python-ish pseudocode, and I'm not an experienced Python developer in the slightest so there might be a more idiomatic way of going about this):

def general_catcher(task):
    try:
       task()
    except SomeError1:
       # do some uniformly-designed recovery stuff here
    except SomeError2:
       # do some other uniformly-designed recovery stuff here
    ...

[Hopefully with a better name than general_catcher]. In this example, you can pass in a function containing what task to perform but still benefit from generalized/unified catch behavior for all the types of exceptions you're interested in, and continue to extend or modify the "what to do" part all you like from this central location and still within a catch context where this is typically encouraged. Best of all, we can keep the throwing sites from concerning themselves with "what to do" (preserving the notion of the "blind thrower").

If you find none of these suggestions here helpful and there's a strong temptation to throw "what to do" exceptions anyway, mainly be aware that this is very anti-idiomatic at the very least, as well as potentially discouraging a generalized mindset.

  • 2
    +1. I never heard of the "Blind Thrower" idea as such before, but it does fit into how I think of exception handling: state the error occurred, don't think about how it should be handled. When you're responsible for the full stack it's hard (but important!) to separate the responsibilities cleanly, and the callee is responsible for notifying about problems, and the caller for handling problems. The callee only knows what it was asked to do, not why. Handling errors should be done in the context of the 'why', hence: in the caller. – Sjoerd Job Postmus Dec 22 '15 at 23:22
  • 1
    (Also: I don't think your reply is C++ specific, but applicable to exception handling in general) – Sjoerd Job Postmus Dec 22 '15 at 23:28
  • 1
    @SjoerdJobPostmus Yay thanks! The "Blind Thrower" is just a goofy analogy I came up with here -- I'm not very smart or quick at digesting complex technical concepts, so I often want to find little imagery and analogies to try to explain and improve my own understanding of things. Maybe some day I can try to work my way to writing a little programming book filled with lots of cartoon drawings. :-D – user204677 Dec 22 '15 at 23:32
  • 1
    Heh, that's a nice little image. A little cartoon character wearing a blindfold and throwing baseball-shaped exceptions out, not sure who will catch them (or even whether they'll be caught at all), but fulfilling their duty as blind thrower. – Blacklight Shining Dec 23 '15 at 9:17
  • 1
    @DrunkCoder: Please don't vandalize your posts. We have enough borken stuff on the Internet already. If you have a good reason for deletion, flag your post for moderator attention and make your case. – Robert Harvey Dec 20 '17 at 16:45
2

I think most of the time it would be better to pass arguments to the function telling it how to handle those situations.

For example, consider a function:

Response fetchUrl(URL url, RetryPolicy retryPolicy);

I can pass RetryPolicy.noRetries() or RetryPolicy.retries(3) or whatever. In the case a of retryable failure, it will consult the policy to decide whether or not it should retry.

  • That doesn't really have much to do with throwing exceptions back to the call site, though. You're talking about something a bit different, which is fine but not really part of the question.. – Lightness Races in Orbit Dec 22 '15 at 17:08
  • @LightnessRacesinOrbit, to the contrary. I'm presenting it as an alternative to the the idea of throwing exceptions back to the call site. In the OP's example, fetchUrl would throw a RetryableException, and I'm saying instead you should tell fetchUrl when it should retry. – Winston Ewert Dec 22 '15 at 17:09
  • 2
    @WinstonEwert: Though I agree with LightnessRacesinOrbit, I also do see your point, but think of it as a different way of representing the same control. But, do consider that you'd probably want to pass new RetryPolicy().onRateLimitExceeded(STOP).onServiceTemporaryUnavailable(RETRY, 3) or something, because the RateLimitExceeded might need to be handled differently from ServiceTemporaryUnavailable. After writing that out, my thought is: better throw an exception, because it gives more flexible control. – Sjoerd Job Postmus Dec 22 '15 at 23:16
  • @SjoerdJobPostmus, I think it depends. It may well be that rate limiting and retry logic make since in your library, in which case I think my approach makes sense. If it makes more sense to just leave that to your caller, by all means throw. – Winston Ewert Dec 23 '15 at 3:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.