22

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 can divide by 0).

What if there is a need 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 that the consumer should just retry the request and forget 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 consumers' usecases. But imagine that in 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?

  • 4
    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
  • 9
    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
  • @KilianFoth - HTTP is chosen here just as an example. Also, the consumer shouldn't know all the HTTP semantics - it just want to get the data. – Roman Bodnarchuk Dec 22 '15 at 16:07
  • 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
  • @ChrisPratt, @RomanBodnarchuk: I think it's a bit of both. If we have a method `fetch` which is supposed to fetch a result over HTTP: yes, it should be prepared to handle the HTTP semantics. If however the HTTP is merely an implementation detail (it's talking to an API that is HTTP based, but a CarrierPidgeon API is also available), it would ideally get a `DataNotRetrievable` error instead of an HTTP (or carrier pidgeon) specific error. – Sjoerd Job Postmus Dec 22 '15 at 23:27
  • @ChrisPratt As Sjoerd Job Postmus noted, HTTP is an implementation details of the `fetch`, and consumers doesn't care how it is implemented. – Roman Bodnarchuk Dec 23 '15 at 14:05
  • Again, I have to disagree. The protocol is very much a part of the API. It could be a REST API over HTTP or a SOAP API, the client needs to know which and needs to know how to communicate in that way. It's not an abstract implementation detail. It's a very real part of working with an API. – Chris Pratt Dec 28 '15 at 16:10
  • @ChrisPratt: Caring how to use it != caring how it's done – Lightness Races in Orbit Dec 31 '15 at 18:06
  • Depends on whether there's actually a difference. Something like a web browser abstracts a lot of logic required to access web content, but typically when you're working with an API, you have a thin client. In that scenario, you *need* to know what status codes mean, how to respond to them, how to format a post body, how to set headers, etc. How it's done (the protocol) is totally intertwined with how it's used. – Chris Pratt Dec 31 '15 at 19:02

4 Answers4

49

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.

Lightness Races in Orbit
  • 8,755
  • 3
  • 41
  • 45
  • 5
    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 02: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 03:02
  • 3
    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 03: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 04:57
  • 1
    @sapi Or you could just document for which exceptions a simple retry is possible. Better, make that apparent from the name (e.g. `ServiceTemporaryUnavailable` is clearly “retryable”; `FileNotFound` is clearly not). – Blacklight Shining Dec 23 '15 at 09:12
  • @sapi yes, this looks like the best way to provide both kinds of information to the consumer – Roman Bodnarchuk Dec 23 '15 at 14:07
  • Just saying: I do http in the background, executing some closure when the outcome is known, so using exceptions wouldn't really work. And anyway, I don't think there are any _exceptional_ outcomes for http / https. – gnasher729 Dec 20 '17 at 22:04
  • @gnasher729: Database not responding? No disk space remaining on server? Why do you think there are no exceptional outcomes for a query made via HTTP(S) transport vs other means? – Lightness Races in Orbit Dec 21 '17 at 11:22
19

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.

ChrisF
  • 38,878
  • 11
  • 125
  • 168
  • 3
    +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 –  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 09: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
3

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.

Winston Ewert
  • 24,732
  • 12
  • 72
  • 103
  • 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 03:35
0

Exceptions should only report the cause of failure. If you have a situation that has a solution, throwing an exception would be inappropriate. That would be using exceptions as a means of flow control and that is bad.

Are exceptions as control flow considered a serious antipattern? If so, Why?

So, what are your options? The most common one is to return an error code. Having your method name start with Try will convey the behavior nicely.

I also like Winston's answer.

Martin Maat
  • 18,218
  • 3
  • 30
  • 57