9

My coworker and I are debating the correct design for an API. Say we have a function void deleteBlogPost(int postId). What should this function do if the blog post indexed with postId does not exist?

I believe it would be appropriate to throw an exception, because the function should be designed to do one thing. When the user calls a function called deleteBlogPost, they always expect the post with ID postId to be deleted. To try to delete a post with an invalid postId does not make sense, so an exception should be thrown.

My colleague argues that the caller does not really intend to delete a specific post, just to ensure that after the call, the post does not exist. If you call deleteBlogPost with a nonexistent post ID, the goal state is already achieved, so nothing should happen. He also noted that this design ensures calls to deleteBlogPost are idempotent, but I'm not convinced that this is a good thing.

We found examples of both patterns in several APIs. For instance, compare deleting a dictionary/map entry with a key that does not exist between Python and Java:

Python:

my_dict = {}
del my_dict['test']   # KeyError: 'test'

Java:

Map<String, Object> map = new HashMap<>();
map.remove("test");   // no exception thrown

Should a function throw exceptions based on its expected behavior or its goal state?

7
  • Lookup idempotency. It very much depends on use cases you design with such function. Your colleague is very probably right about that. Feb 18, 2020 at 18:22
  • What are the requirements of your system? What does the client of your system (human or otherwise) expect if the blog post doesn't exist? Feb 18, 2020 at 18:47
  • As your colleague stated, it's all about intention. If your intention is to delete a post, it's probably useful to know whether or not it existed in the first place, especially if you want undo capability. If your intention is to merely ensure a post does not exist, doing nothing on subsequent attempts with the same id is a reasonable approach. FWIW I would probably name a method following the former approach differently: something like ensureNotExists(). Feb 18, 2020 at 19:20
  • 1
    I would argue that both of you are wrong. Your colleague is wrong because deleteBlogPost would need to be named something like ensureBlogPostIsDeleted. Your also wrong because its actually not an exceptional circumstance for a blog post id to not have an existent blog post. It is an actual and legitimate scenario. Therefore the function should return that the blog post did not exist, when the id did not have a blog post.
    – Kain0_0
    Feb 19, 2020 at 1:08
  • 1
    If result of throwing an exception will be that all consumers are forced to wrap it with try/catch, then I would go with "do nothing for non existing id".
    – Fabio
    Feb 19, 2020 at 22:15

8 Answers 8

8

Should a function throw exceptions based on its expected behavior or its goal state?

Neither. This is a false opposite.

The proper criteria for whether to throw an exception is whether it is due to exceptional circumstances.

Java:

You should only use exceptions for exceptional situations. An exceptional situation is an unexpected situation that is out of the ordinary. A situation is not exceptional just because it's less common than other situations. In a non-exceptional situation you should use return values instead of exceptions.

c#:

Exceptions should not be used to change the flow of a program as part of ordinary execution. Exceptions should only be used to report and handle error conditions.

So the real question is whether you expect the file to be missing and if that is a normal business case, given the context and requirements, or if it is some kind of exceptional behavior that you wouldn't normally expect.

I can think of reasons to do it either way. In a highly parallel system, for example, if it's often the case that several threads are deleting files at the same time, you might want deleteBlogPost to fail silently. On the other hand, if the blog post is a sensitive item and a one-time deletion is required, and the deletion event is logged and auditable, it might be better if deleteBlogPost threw an exception, to ensure data consistency.

5
  • 3
    "The proper criteria for whether to throw an exception is whether it is due to exceptional circumstances." - FWIW, the question is marked language-agnostic, and this claim is not always true. In some languages throwing exceptions for ordinary events is the way things are properly done. Feb 18, 2020 at 19:58
  • @whatsisname please provide an example of a language where an Exception is not intended for Exceptional Circumstances.
    – Roman Mik
    Feb 18, 2020 at 22:08
  • 1
  • @john-wu maybe you can update your answer with an example for Python ? The Pythonic philosophy is relevant to the OP's Question, imho
    – Roman Mik
    Feb 19, 2020 at 14:29
  • @whatsisname even in some languages where it is normally done as explained, there are frameworks that make it the norm to use them for "expected" failure behaviour, so yeah, the pirate err programmer codex is more a set of guidelines rather than rules. Example: With a standard springboot application in Java it is quite beneficial to use exceptions also for invalid user input and the like, because they can be directly mapped to the matching HTTP response codes (if you want to use HTTP response code for business logic, which one can fight over as well). Feb 19, 2020 at 21:04
4

If you absolutely must know, right now, this very moment, that the delete succeeded in making something into nothing you can still do it different ways.

One is to throw. Throwing an exception is slow (compared to alternatives). Stack traces don't concatenate themselves for free. Because of this we code monkeys have a tradition of making throwing exceptionally rare. So beware of forcing people to catch, what may be for them, non errors. That may confuse people who are used to the tradition. But this way makes the event hard to ignore.

Another design is to return. This avoids concatenating anything. It just looks weird. People have trouble with the idea of nothing. Getting something when making it into nothing seems strange. But if you must confirm that the nothing used to be something, atomically, then return the something. Let the user put it somewhere safe and test it if they care so much. Done this way exceptions aren't needed and all the same use cases work. It just looks freakin weird.

Since you can't know exactly how your API will be used you have to decide what you will support. If you want to support it all, a name change can make things seem less weird:

Remove a key from dictionary using dict.pop()

dict.pop(key[, default])  

If key exists in dictionary then dict.pop() removes the element with given key from dictionary and return its value.

If given key doesn’t exist in dictionary then it returns the given Default value.

If given key doesn’t exist in dictionary and No Default value is passed to pop() then it will throw KeyError

thispointer.com - Different ways to remove a key from dictionary in python

Now the way it works is less strange and the API users can decide for themselves.

If you absolutely do not care, ever, whether the delete changed anything, so long as nothing remains, you're looking for idempotentcy.

Using pop (with default) will allow an API user to achieve idempotentcy by simply ignoring the returned value.

The relevancy of idempotency (ability to call it repeatedly and expect the same result) is something the API user has to determine. All you can determine as an API author is if it's worth the trouble of supporting that.

7
  • Throwing an exception can be slow (for some definition of "slow"), but unless you're in a tight loop, the semantics of throwing an exception is usually a more important consideration. Feb 18, 2020 at 19:56
  • @RobertHarvey true. Readability should never be forgotten. But the rub with being an API author is you have no idea if you're in a tight loop. So it becomes a question of 'do we want to support tight loops on atomic deletes that need to detect if they result in a change of state'. If you do, please try to keep it from being ugly. Feb 18, 2020 at 20:07
  • The cost of throwing is highly exaggerated in my experience. The cost of it is proportional to the depth of the stack. I did some experimentation on this years ago in Java and my recollection is that you needed to be hundreds or thousands of calls deep into the stack before the cost was significant.
    – JimmyJames
    Feb 18, 2020 at 20:18
  • @JimmyJames better? Feb 18, 2020 at 20:24
  • @candied_orange It wasn't really bad to begin with. It's just something I think has a unwarranted reputation and get's thrown around when people don't like exceptions. It's actually the case that you can be thousands of calls deep in a lot of frameworks so YMMV.
    – JimmyJames
    Feb 18, 2020 at 20:33
3

Both methods have their pros and cons, and each has situations where it is applicable. I prefer the "no throw" version as this is easier to handle during cleanup. If I try to remove something, I usually don't care if the object was in the container or not; I just want to ensure it isn't in there anymore. If the remover is going to throw if it isn't found, then some types of cleanup code will need to have try/catch blocks and/or check if the object is in the container first. It is also easy to forget to add those handlers or checks, resulting in a cascade of issues when this condition isn't handled.

A third possibility is to have the remove function return an indicator if an object was removed or not. It can be a simple boolean value, or a count of removed objects. This would allow the caller of the remove function decide how to handle the case where the object was not found, without requiring additional overhead in cases where we just want to ensure it isn't there.

1

There are multiple ways how you can define the outcome of a function void deleteBlogPost(int postId).

  • You can define outcome as operation that was performed. Here, this would give the two outcomes post deleted and post not deleted.
  • You can define outcome as postcondition. Here, this would give the two outcomes post does not exist anymore and post still exists.

Unfortunately, both definitions are lying to you. They communicate that there are only two possible outcomes and often, developers interprete them as success and failure. You and your coworker fell into the same trap, but you do not agree on what is success and what is failure. The truth is that the function has more outcomes:

  • Post deleted.
  • Post not deleted because it did not exist.
  • Post not deleted because of error (e.g., permission).
  • Post not deleted because of another error (e.g., corrupt database).

If you try to categorize these into success and failure, you will always run into disagreements about which category post not deleted because it did not exist belongs to. Unfortunately, the signature void deleteBlogPost(int postId) only leaves room for two categories. It can either succeed or it can fail (via exception). The signature does not communicate that more things can happen.

If you want to communicate correctly, you need to change the function signature. You can, for example, change the return value to an enum with the values POST_DELETED, POST_DID_NOT_EXIST, PERMISSION_ERROR, CORRUPT_DATABASE and remove the exceptions. This way, all outcomes are clearly communicated. However, a disadvantage is that the function may fail silently (because there are no exceptions anymore).

You can keep the exceptions by adding an argument that controls the exception behavior. For example, you can add an enum eExceptionBehavior with the values THROW_IF_POST_NOT_EXISTS and DONT_THROW_IF_POST_NOT_EXISTS (or something more readable). If you call the function like this

deleteBlogPost(42, THROW_IF_POST_NOT_EXISTS);

everyone knows what's going on.

0

I'll throw in my opinion that both approaches are valid and can be considered correct. The question is which approach fits your needs.

If you treat it as an error, it may help you identify bugs in your application more quickly. However if the situation can result from the normal execution of the program, it creates its own issues.

I recall a very annoying 'feature' of a Oracle driver where it would throw an exception when you attempted to close a connection to the DB when it was already closed. It was something that would just randomly occur every once in a while during the close procedure. There was no real issue, though. The goal was to close the connection and it was closed. Nothing more needed to be done.

On the other hand, if the only way that this situation can occur is due to some sort of programming or execution error, I'd lean towards throwing an exception. It might save you some time debugging.

11
  • You can help debugging by logging the situation instead if throwing.
    – Andy
    Feb 18, 2020 at 22:25
  • @Andy Squash the exception? I don't agree. That's the opposite of helpful.
    – JimmyJames
    Feb 19, 2020 at 15:48
  • If your concern is debugging only, then logging seems more appropriate (which isn't squashing the exception). If you want something to be deleted, and its already gone, why does that actually matter to the user? They got what they wanted
    – Andy
    Feb 19, 2020 at 15:50
  • @Andy Read the answer. If the only way this can occur is because of some sort invalid state, you are better off finding out right away.
    – JimmyJames
    Feb 19, 2020 at 15:52
  • @JimmyJames I was going to agree that you could treat it as an error to help catch bugs, creating your own expectation that the caller knows the post exists and there's no race to delete it, but then I realized that's more of a meta-expectation than a real expectation. If the caller wants to enforce such an expectation on themselves then they can do so but I think that shouldn't be enforced by the method itself.
    – xtratic
    Feb 19, 2020 at 18:36
0

Exceptions are, as the name already implies, intended to communicate exceptional situations; you could also say "unexpected situations". So the question is:

Is it unexpected that a request is performed to delete a post that doesn't even exist?

Exceptions are not intended to communicate expected failures but it's up to you to decide what is an expected failure and what not.

E.g. if I'd write a HTTP framework, getting a 404 page not found error would be an expected failure to me when trying to fetch a resource as I can impossibly know if the resource exits prior to fetching it. An exceptional failure would be if the TCP connection dies in the middle of the transfer of the resource.

If you say "You must not delete a post that doesn't exist", as that would be a programming error, then trying to do so should throw an exception.

If you say "It is okay to attempt to delete a post that doesn't exist as you cannot even know in advance if it (still) exists", then it would not be an exception if the post doesn't exist, yet it would be an exception if the connection to the database is lost when attempting to delete it as that is not expected to happen.

If you need to know if a post was deleted or not, as the fact that a post couldn't be deleted requires special handling, communicate success/failure:

bool deleteBlogPost ( int postId )

Function returns true if a post was deleted, false otherwise and throws exception if something else went wrong.

0

It's the goal state. But also...

It doesn't matter

Conceptually, you can think that if you cannot clean up a mess, you need to throw an exception. Cleaning up the mess is as ambiguous as it sounds. But let's make it clear and simple:

The unfortunate adoption of exceptions as a programming construct can introduce more problems than it solves. So the answer to your question is that it does not matter. Based on what you want to design and model, you decide what you handle and what you don't handle. For whatever it is that you decide to not handle, you must throw an exception. And not only that, you must also inform everyone else using your API that, in those cases, you do throw an exception.

  • Code represents instructions. Exceptions represent the lack of instructions.

Whenever you write code, you use signatures for your methods and behavior. These signatures guarantee what someone can expect from you. Exceptions mean that you cannot handle a situation in any reasonable way, based on what you purport you can do. Methods/Functions are not entirely inanimate black boxes. They encode domain knowledge in the form of behavior. Usually, they model a behavior, so the best advice can only come from the modeled domain/behavior. Exceptions were created to make our life easier.

  • Our method signatures document what we do. Exceptions supply the complement by documenting what we don't.

But in the end, it doesn't matter. However you design your method, people using your API will get used to it and use it, as you have seen with the various patterns used by different languages. It doesn't matter when or whether you throw an exception, as long as:

  • You communicate properly all cases where you may throw an exception (e.g. Java's throws keyword), so that everyone is prepared.
  • You provide a fair alternative as soon as you start noticing things like:
try
{
    double x = double.Parse(str);
}
catch (Exception ex) //Or any other exception
{
    //...
}

or

if (dict.ContainsKey(key))
{
    var value = dict[key];
    //...
}

in dozens of locations across a codebase utilizing your API.

-2

It is debatable whether the item not being there can be regarded as success. The caller may believe there is one less item after the call and build on that. Interpreting what the caller really wants or should settle for is speculative. The delete operation failed, period. If that kind of behavior is too rigid for you, call the method TryDelete instead and return a result indicating success or failure.

Because your function does not return a result you should throw. Idempotency is irrelevant in this context.

1
  • 2
    The calling code should have no idea how items are stored or care about their count.
    – user949300
    Feb 19, 2020 at 7:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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