Hello everybody. I am fighting internally with Rom...
# getting-started
t
Hello everybody. I am fighting internally with Roman's article about exceptions: https://elizarov.medium.com/kotlin-and-exceptions-8062f589d07. I am not convinced about main parts of the article and would like to know how you guys handle them in real-world applications. Especially what type of exceptions you declare in KDoc. Whether you go deep within your function call stack and try to be as exhaustive as possible about exceptions. And on the caller side, whether you simply have
try { } catch(e: Exception) { }
just to be sure you don't miss anything even if the bottom API change?Thanks.
m
we avoid exceptions like the plague. Exceptions are only for interop outside your own codebase and shouldn't be used to jump within your own code. Our interaction with these outside worlds, like an API, database or file-reading, is handled by an external library that we use. We try to handle all their exceptions, if any, immediately at the first place of interop. What are you not convinced by exactly?
💯 1
t
An alternative opinion, in Kotlin Exceptions are a tool for Dependency Inversion of errors paths.
If it’s reasonable for the consumer of a function to handle the error, don’t use an exception. If it’s unreasonable for the consumer of the function to handle the error, use an exception.
For example your business logic should most likely not handle network issues.
Those can happen in exceptions to pass through/around the business layer to another low-level layer that’s more appropriate to handle it.
More likely than not, your own code will not have a need to raise these kinds of errors though.
Handle them yes, raise them likely not
o
I agree with Roman's article. There are very few exceptions I catch and handle beyond logging and shutting down. In most cases, these are I/O errors, which may also include network errors and concurrent (database) access conflicts. Maybe this article I posted a while ago also helps: https://stackoverflow.com/questions/63535681/what-should-a-coroutineexceptionhandler-do-with-an-outofmemoryerror-or-other-fat/63569145#63569145
👋 1
👍 1
j
app and modules-> Either and never throw exceptions, library -> exceptions
t
Sorry for late response. Got lost in time.
Tim Oltjenbruns:
If it’s reasonable for the consumer of a function to handle the error, don’t use an exception. If it’s unreasonable for the consumer of the function to handle the error, use an exception.
Well, in case you want to use coroutines and their automatic cancellation, you are FORCED to use exceptions. So no
Either
or similar constructs to save the day. So basically you all do not bother with exceptions. Which means that any kind of exceptions can go through you business service layer (I/O, DAO, SecurityException, 3rd party exceptions, ...) just like there are no boundaries between application layers. And then what? You create numerous handlers (and manually specify all possible exceptions) to react accordingly? The problem goes bigger if you have multiple implementations of a service and each implementation can throw different exceptions. How do you document such a service which can actually throw the whole universe of exceptions? Do you even document them? I do not like the idea that my service layer can throw any kind of exceptions without documenting what can go wrong. And wrapping underlying exceptions to some service-like exceptions doesn't fit to "not bother with exception".
t
My point was that you should only throw an exception if you intend it to cross between boundaries. If you do not want it to cross boundaries, do not use an exception for it. If you are expecting an exception from across a boundary, such as a third party library, and do not think its reasonable to pass it through your component’s boundary, then catch it as soon as possible similar to what Michael said.
In my mind, exceptions are for when the caller should not know about your error, but instead something across an interface boundary, to something at or below the level of the thrower.
For example if you are implementing an interface that is owned by a business object of some kind, you do not have control to return any kind of error that’s lower-level than the business object. (Or at least, you should not). The business logic should not be concerned about a broken pipe, unless your domain is networking. It may be reasonable to throw an exception and stop the business logic in its tracks. This breaks encapsulation, but it may still be reasonable for the presentation logic of said business logic to know what exceptions may be thrown. This is less bad than breaking encapsulation in another way, because the presenter and service are kinda in the same “level” as each other. A common alternative to this is muddling the business logic with something to unwrap/rewrap the networking error anyways, so the presentation object isn’t breaking encapsulation anymore, technically. But you still have the end result of the presentation logic knowing about the networking errors. And the business logic isn’t as high level anymore. Quick diagram of a situation where I think exceptions may be appropriate. The cancellation point you bring up falls into this same bucket I think.
TLDR: exceptions are for dependency inversion, when you cannot simply swallow and ignore an exception while implementing an interface.
t
I totally agree with everything you said. And yet, we read (from Roman's article):
As a rule of thumb, you should not be catching exceptions in general Kotlin code. That’s a code smell. Exceptions should be handled by some top-level framework code of your application to alert developers of the bugs in the code and to restart your application or its affected operation. That’s the primary purpose of exceptions in Kotlin.
and:
You should design your own general-purpose Kotlin APIs in the same way: use exceptions for logic errors, type-safe results for everything else. Don’t use exceptions as a work-around to sneak a result value out of a function.
And many other statements. What is the point of throwing
IllegalArgumentException
from a business layer without any context? Next time you choose another implementation of your service interface and now an
IOException
may be thrown from your business layer. I would like to defining a strict description of my business layer with business-specific exceptions without leaking any lower ones but this require many
try { } catch(LowLevelException) { /* throw BusinessException */ }
blocks which is a code-smell according to others. So this is where my internal struggle with exceptions in Kotlin and disagreement with the article come from.
t
If it's an error that should be handled directly by the caller, an exception may be a code smell in that case. But if that's not the case, it's not so black and white. I agree with you somewhat here, mostly that the opinion in the article is drawing too broad of an assumption. But I may disagree with throwing exceptions from business logic. Business logic isn't likely to be called from behind an inverted dependency. Unless in your system it has a good reason to, in that case I say carry on!
o
@tomas-mrkvička
So basically you all do not bother with exceptions. Which means that any kind of exceptions can go through you business service layer (I/O, DAO, SecurityException, 3rd party exceptions, ...) just like there are no boundaries between application layers.
Well, I do bother. Examples: • I handle I/O exceptions at the level where these are expected and can be dealt with appropriately. • I handle specific exceptions originating in the business infrastructure layers (concurrent access conflicts, for example). These are similar to I/O exceptions in the sense that they can be raised at multiple points in a complex sequence of actions, but can be dealt with in a centralized way. These exceptions are not simple wrappers of lower-level exceptions, but provide relevant information to users about effects and causes. And, of course, such exceptions are part of the originating layer's interface, and properly documented. • Then there is the rest: An unlimited universe of non-recoverable exceptions, errors of all kinds. These I catch, log and recover from at the highest level, possibly via a full shutdown and restart. Due to their unlimited nature and non-recoverability, they are not documented.
t
After thinking about this for a bit longer, the examples I can think of that are too low-level for business to care about could probably all be expressed with a cancellation(exception)
If for some reason any of my lower level code on the other side of the business logic did care about a specific exception (rather than the mere presence of an exception), then I would be fine throwing one. But I can’t think of any.
m
There is always the possibility of encapsulating a function return within a Result datatype. In our project we have a datawarehouse communicating data with us via Fhir. Lets say they want to delete a client: They do a call to us, we call the repository, the repository has a constraint violation or something similar, we encapsulate that within the result. We then fold this "result" type to a response and communicate the error specifically how we want. We also have a test interfacing where testers can click around in a mockup software. Here we might handle this result type differently. We also might have some overarching business logic, that needs to know if an operation succeeded in order to go through and thus, thats another way of handling it. In all cases you know what kind of exceptions you want to handle, and other edgecases we just throw anyways and handle on a higher level (out of memory or other errors)
t
Right and often the result is a good place to put the error
but not all errors belong in the result type.
only business-related errors should show up there
(if the consumer of the result is business logic)