Also general kotlin question from a relative newbi...
# announcements
p
Also general kotlin question from a relative newbie: several months in, I still feel like I don’t fully grok kotlin’s position on the function of exceptions. There is this blog post: https://elizarov.medium.com/kotlin-and-exceptions-8062f589d07 that seems to be basically saying that you should only use exceptions where you’d use assert() (ie, to signal logic errors), and most of the time you shouldn’t be catching exceptions --- ie, try/catch blocks are a code smell. There are some obvious … exceptions to that rule (such as, catching exceptions thrown by java code), but --- fair enough, error states should be signaled through special return values, either null or a special instance of a sealed class. … but then I go out into the world, and I don’t see that pattern being followed. For instance, ktor (which a number of blog posts have nudged me towards as a good sane default http client for kotlin) by default uses exceptions to signal http error codes. IE --- rather than exceptions indicating a bug, ktor throws exceptions to indicate that a potentially expected error has occurred. I’m fine with this pattern, too! But it makes it hard to see what I should adopt in my own code.
👀 2
I do miss the rich language for communicating errors that exceptions provide. I understand why they’re more dangerous to use/fit the “probably don’t bother to catch them” pattern of kotlin, where they’re not built into the type signature of the functions that throw them.
But I don’t feel pushed by the language towards a general purpose replacement pattern for communicating richer error state to the caller (for instance, the Result class in the standard library isn’t available for me to use and contains a reference to an actual exception instance) and I feel like I see inconsistency in how popular libraries handle this, too.
And now I feel like I am repeating myself and will quietly wait for a response 😆
r
Since I like this subject and I happen to be online, I’ll bite 🙂 I believe I’ve read a few places that a generally recommended idea is to use sealed classes to model appropriate success / failure modes for your situations, rather than try to use a truly generic structure for all things. The idea being that the language is brief and expressive enough that handling things that way is easy and more powerful. Personally, as an advocate of “fear the power of unexpected stack unwinding!!!!” I’m down with that. I’ve definitely rolled a few more generic “Result” types as well.
Agreed that the community doesn’t seem to be clearly figuring this out though, which IMO is likely because of all the Java heritage
For example, here’s one I rolled for certain kinds of API communication that I use in my personal projects:
Copy code
sealed class Result<V>
data class SuccessfulResult<V>(val value: V) : Result<V>()
data class NotFoundResult<V>(val entityName: String) : Result<V>()
data class ErrorResult<V>(val message: String) : Result<V>()
class UnauthorizedResult<V> : Result<V>()
Def not perfect, but now I know everything coming through this process meets one of these forms. And if I need a new form, I add it, or refactor somehow.
p
I tend not to fear the power of unexpected stack unwinding, after having spent many years writing python where that’s idiomatic and should never be unexpected 😆 and so I miss that pattern, though I know there are others that have strengths too. Sealed classes are great and powerful, and I do like that answer, but I wish the language had syntactic sugar built into it to encourage that pattern. As it is, using that pattern obscures the actual return type of functions where it’s used, and unpackaging a Result from the caller so as to get the successful result if possible and return an error otherwise feels a little awkward and boilerplatey (though that may just be me getting used to a new way of doing things). That, and there’s no standard for what “success” and “failure” should be called.
r
Yeah I definitely added a transform extension function to that result so code that only deals with the success case… can
p
also, thanks for responding! I appreciate the perspective and the chance to talk/think this stuff through
r
there’s some depth to finding clean patterns that both provide the value of having the options, and also make it easy to handle. consolidating things that can fail together can help substantially
👍 1
p
I guess I just feel a little lost because since the language isn’t opinionated on the subject, I find myself having to invent an answer for each thing…. and tempted to conflate error states so that I can just punt and return null 😆
r
I’ll spare you the rant about stack unwinding 😉
Yeah, I don’t think there’s anything stopping you from doing that
and at the moment you find yourself needing to inspect that error, then you can consider if you want to go with something more precise
p
I mean I’d be curious! It feels like kotlin still has stack unwinding, it just feels scarier because it’s only supposed to happen in case of disaster and it’s only supposed to be caught at the top level
r
I adore that kotlin sealed classes enforce completeness on ‘when’ statements
p
yeah that’s great
r
yeah it does
its like the philosophy of golang errors, but without the scary rename of exceptions
(golang renamed exceptions into panics, and then shunned them… to the wastelands! but they still have them for some things)
p
I guess the reason I ended up feeling frustrated about error handling is because I love the things kotlin does with its type system --- completeness across sealed classes, null safety built in (and all the nifty, slick ways of converting between nullable and non-nullable types and handling that logic), the way delegation works, etc
it’s elegant and pretty! And so (again as a very non-expert 😆 ) I’ve found it surprising that the same seems not to apply to error handling, where the language has sort of booted it out of the tyep system seemingly
but I know things are still rapidly changing and evolving!
r
i think the mental shift is more along the lines of “errors aren’t special, and many things traditionally thought of as errors are really just normal pathways”
p
That’s fair. And a useful framing that I need to shove my brain into 😆
Anyway, thanks for the response and the conversation 🙂 It’s given me some things to think about
r
🙂
took me a while, but I traced down that I use that particular Result object to decide which react component to show based on an API call
happy to chat!
😄 1
p
hehe
h
The exact same discussion and the exact same examples have been brought up before :) the thing is... a) Java legacy, you already talked about that. b) when sealed classes are not sufficient, you would need sth like kotlin-result in the std lib.. there is kotlin.Result but its not ready yet, so it cant be used universally yet. Regarding ktor: there is https://ktor.io/docs/response-validation.html where you can disable exceptions, but the thing is... How would one model http status codes properly with result and/or sealed classes. For example whether 200 is success or not depends on context... Or take client errors, happen rarely and most often cant be handled locally... So better modeled as exceptions i guess. I think when the time is right we get result that is generic over the failure type in the std, but it wont enhance http stuff - would be happy to be proven wrong with this.
c
@Hanno I've been using Ktor with Kotlin Arrow (#C5UPMM0A0), and the
either
block helps a lot. Essentially, it let's you have short-circuiting operations without exceptions. For example:
Copy code
val result = either {
  val id = call.parameter(...).bind() // if fails, short-circuits the block
  val token = call.parameter(...).bind() // same
  db.doStuff(token, id).bind() // same
}
// Now, 'result' is either a failure (my own sealed class hierarchy of what could go wrong), or a successful value
This whole syntax abuses
suspend
quite a bit, but it let's you code as if you had exceptions, but you don't, it's all an
Either
return type.
Here's a code example: https://gitlab.com/braindot/clovis/-/blob/master/server/src/main/kotlin/clovis/server/Ping.kt#L13 (it uses the old syntax
!
instead of
.bind())
h
Yea, i know arrow, kotlin-result has a similar feature for comprehensions. What i wanted to say is that you dont get the usual benefits with the result type here, because most failures are better modeled as exceptions because of mentioned reasons.
j
I’m not sure I’d use ktor as a “sane default” for anything
😂 1
p
@Jordan Stewart is there something you’d recommend instead?
j
Sorry, that was a bit of a flippant response! I’ve found ktor a bit painful to work with. That said, I’ve mainly used the server rather than the client so perhaps should give the client the benefit of the doubt rather than just assuming that it’s as painful to use as the server is. (I can elaborate on the server pain if that’s helpful 🙂) I encountered okhttp a few years ago when I joined a project using it. I’m now using it on a different project and am pretty happy with it. Prior to this I used Apache HttpClient and compared to that (as of ~2015 so YMMV) I found that okhttp had better default configuration, was easier to avoid resource leaks with, and easier to dig into on occasions where you want to work out exactly what’s going on. In both cases, instead of using the okhttp api directly it was wrapped in http4k — a functional/immutable http facade. In fact on the first project we had both Apache HttpClient and okhttp coexisting for a while (in different services!) I really like the simple immutable api http4k exposes, and it makes testing a breeze compared to e.g. ktor’s server. As far as your original question goes, as you’ve noticed already there’s not currently a generally accepted “Kotliny” approach to error handling. The best we can do is probably to choose one approach and then adapt libraries you use to that approach so that at least error handling is consistent. (e.g. by adding a wrapping layer if necessary.) An overview of different error approaches that I found really interesting is this blog series: http://www.oneeyedmen.com/failure-is-not-an-option-part-7.html
p
thank you! I appreciate the guidance!
okhttp was also on my radar --- I just went with ktor after reading some blog posts because I don’t really know the landscape yet
and good to know!
h
@Jordan Stewart i am not the op but what where your pain points with ktor server?? I really liked it.
j
Quite a few different things really 🙂. No difficulties I couldn’t overcome, but enough to add quite a bit of friction to the development process. A few of the more annoying things I ran into: I found adding custom steps to the http interceptor chain difficult to work out (e.g. which ones are called, and when.) Requests are immutable, which is great, but are also an immutable part of the context rather than a parameter that’s passed between interceptors, so you can’t for example transform headers. So for example we have Pact tests and instead of putting an opaque JWT that will eventually expire into tests that require authentication I wanted to add a custom header, e.g.
Test-User: <some user id>
, and then layer a custom interceptor on top of the standard http stack to replace this with a
Authorization: Bearer <a dynamically issued JWT for 'some user id'>
header. This would leave the standard http stack untouched, make the test really clear about the authenticated user’s id (no need to decode a jwt to see the user id), etc. With Ktor I couldn’t see how to modify the request object, so instead of simply modifying the request I had to add an ApplicationCall attribute in my interceptor and then have the standard http stack look for this attribute and treat whatever is in it as a trusted identity assertion. I guess this is probably ok from a security perspective (as long as client code can’t ever arbitrarily inject these attributes) but it’s certainly not as good as what I wanted to do in the first place, which is really easy to do in http4k since requests, whilst immutable, are passed as parameters between http interceptors. I also found testing some stuff painful as everything is tied together, e.g. you can’t just create a request, as it needs an ApplicationCall, which needs an Application.
h
Ah yes i c your pain. Thx