8 ms·
Result is all I need
- anon-3988 1y agoWith the ? syntax in Rust results and exceptions are the same thing. I posit that the former is superior. It is unfortunate that results have worse performance but I don't see any reason why. Results that bubbles up all the way ought to be identical to an uncaught exception.
- rcxdude 1y agoExceptions can tradeoff happy-path performance for more overhead on the slow path. For example, an exception implementation can make it so that callers can assume the 'Ok' result always appears, because an exception causes a seperate unwinding mechanism to occur that walks the stack back, bypassing that entirely. In contrast every caller to a function that returns a Result must have a branch on that result, and this repeats for each part of the callstack. This also means that exceptions can have stacktraces that only incur a cost on the unhappy path and even only if that exception is uncaught. While if you want a trace for a bad Result you are going to be doing a lot of extra book-keeping that will be thrown away In general I agree that Results are the better abstraction, but there are sadly some tradeoffs that seem to be hard to overcome.
- thinkharderdev 1y agoThis depends a lot of what you are using exceptions for. I think in general the branch on Ok/Err is probably not meaningful performance-wise because the branch predictor will see right through that. But more generally the happy-path/error-path distinction can be a bit murky. From my days writing Java back in the day it was very common to see code where checked exceptions were used as a sort of control flow mechanism, so you end up using the slow path relatively frequently because it was just how you handled certain expected conditions that were arbitrarily designated as "exceptions". The idea behind Result types to me is just that recoverable, expected errors are part of the program's control flow and should be handled through normal code and not some side-channel. Exceptions/panics should be used only for actually exceptional conditions (programming errors which break some expected invariant of the system) and immediately terminate the unit of work that experienced the exception.
- _ZeD_ 1y agogosh... try { val user = authService.register(registrationRequest.email, registrationRequest.password) return user } catch (exception: Exception) { // log exception throw exception } no, no, no! the whole point of the exceptions (and moreso of the unchecked ones) is to be transparent! if you don't know what to do with an exception do NOT try to handle it that snippet should just be return authService.register(registrationRequest.email, registrationRequest.password)
- rockyj 1y agoI agree, this was just a sample code to show how usually imperative if / else / try / catch code is written. What is also possible is we catch the exception, log it and throw another one.
- CGamesPlay 1y agoI'm gonna plug my favorite note on this topic: https://ericlippert.com/2008/09/10/vexing-exceptions/ https://ericlippert.com/2008/09/10/vexing-exceptions/ Both snippets suffer from being too limited. The first, as you point out, catches too many exceptions. But the second.... What happens if the email address is taken? That's hardly exceptional, but it's an exception that the caller has to handle. Your natural response might be to check if the email address is taken before calling register, but that's just a race condition now. So you really need a result-returning function, or to catch some (but probably not all) of the possible exceptions from the method.
- noduerme 1y agoThe way I usually structure it is that the only exception would be some type of failure to connect. Any actual error thrown by the service comes back as a result.error, and any failure (like email address taken) comes back as result.fail. This way you can separate it into (1) connection problem, (2) backend error/bug/database offline/etc, (3) both of those are fine but the backend doesn't like the input.
- jillesvangurp 1y agoSince this looks like Kotlin, worth pointing out that there is a kotlin class in the standard library called Result. I've been using that for a few things. One place that I'm on the fence about but that seems to work well for us is using this in API clients. We have a pretty standard Spring Boot server with the usual reactive kotlin suspend controllers. Our api client is different. We were early adopters of kotlin-js on our frontend. Not something I necessarily recommend but through circumstances it was the right choice for us and it has worked well for us in the last five years. But it was a rough ride especially the first three of those. As a consequence, our API client is multiplatform. For every API endpoint, there's a suspend function in the client library. And it returns a Result<T> where T is the deserialized object (via kotlinx serialization, which is multiplatform). On the client side, consuming a result object is similar to dealing with promises. It even has a fold function that takes a success and error block. Basically failures fall into three groups: 1) failures (any 4xx code) that probably indicate client side bugs related to validation or things that at least need to be handled (show a message to the user), 2) internal server errors (500) that need to be fixed on the server, and 3) intermittent failures (e.g. 502, 503) which usually means: wait, try again, and hope the problem goes away. What I like about Result is making the error handling explicit. But it feels a bit weird to client side construct an Exception only to stuff it into a Result.error(...) instead of actually throwing it. IMHO there's a bit of language friction there. I also haven't seen too many public APIs that use Result. But that being said, our multiplatform client works well for our use. But I can't expose it to Javascript in its current form; which is something I have been considering to do. This is possible with special annotations and would mean our multiplatform client would be usable in normal react/typescript projects and something I could push as an npm. But the fact my functions return a Result makes that a bit awkward. Which is why I'm on the fence about using it a lot. So, nice as a Kotlin API but good to be aware of portability limitations like that. You would have similar issues exposing Kotlin code like that to Java.
- rockyj 1y agoThank you, a very insightful comment :) As a side note, my latest post (on the same website) is on "reactive" Java / suspend functions in Kotlin.
- 1y ago
- phplovesong 1y agoThis is why Haxe is awesome. You can target a sloppy langauge and still get the benefits os a ML-like typesystem.
- paduc 1y agoFor the typescript world, there is neverthrow[0] which offers a similar Result type. [0]: https://github.com/supermacro/neverthrow https://github.com/supermacro/neverthrow
- pshirshov 1y agoPromise<Result<number, string>> everywhere or type EitherT[F[_], E, A] = F[Either[E, A]] and then def dosomething(): F[String, Number] ? Isn't this beautiful: https://github.com/7mind/distage-example/blob/develop/bifunctor-tagless/src/main/scala/leaderboard/services/Ranks.scala https://github.com/7mind/distage-example/blob/develop/bifunc... ?
- RedNifre 1y agoWhy does it have Either? Doesn't TypeScript have "A | B" style sum types?
- seivan 1y ago[dead]
- pshirshov 1y agoEither is biased, union is not. Probably we should say "union" instead of sum, as typescript unions are not discriminated. string | string in typescript is exactly the same as just string, while Either[String, String] is a type which is exactly a sum of two string types. Plus Either is biased towards R, the happy path value.
- pshirshov 1y agoAh, Either. Didn't recognize you from the first glance. Now we need to invent do-notation, higher kinds and typeclasses and this code would be well composable.
- tyteen4a03 1y agoSmells like something that Effect-TS is designed to solve in the TypeScript world.
- flaie 1y agoGood article. Maybe you could look up the Try monad API (Scala or Vavr works in Java + Kotlin), by using some extra helper methods you can have something probably a little bit lighter to use. I believe your example would look like the following with the Try monad (in Java): public UserDTO register(UserRegistrationRequest registrationRequest) { return Try.of(() -> authService.userExists(registrationRequest.email)) .filter(Objects::isNull, () -> badRequest("user already exists")) .map(userId -> authService.register(registrationRequest.email, registrationRequest.password)) .get(); } The login() function would be using the same pattern to call authService.verify() then filtering nonNull and signing the JWT, so it would be the same pattern for both.
- lachenmayer 1y agoStick your services into the type too, and you have `Effect`[0], the ultimate monad :) [0]: https://effect.website/ https://effect.website/
- topspin 1y agoI've been toting around and refining a Result<> implementation from project to project from Java 8 onwards. Sealed classes in 17+ really make it shine. I wish Oracle et al. had the courage to foist this into the standard library, damn the consequences. Whatever unanticipated problems it would (inevitably) create are greatly outweighed by the benefits. I've written Pair<> about a dozen times as well.
- oweiler 1y agoUse the builtin Result class and runCatching/fold and be done with it. Yes, it has shortcomings but works well enough in practice.
- byteshiftlabs 1y agoI love how everyone here shares real experience with Kotlin and Result, it’s cool to see different views that actually teach something.
- time4tea 1y agoResult<T> is a built-in in kotlin, but this enforces that the error type is a Throwable If you fancy that an error could be just a type, not necessarily a Throwable, you might like Result4k - it offers a Result<T,E> https://github.com/fork-handles/forkhandles/tree/trunk/result4k https://github.com/fork-handles/forkhandles/tree/trunk/resul... disclaimer: I contribute to this.
- RedNifre 1y agoNice, what's the KMP plan there? We currently use https://github.com/michaelbull/kotlin-result https://github.com/michaelbull/kotlin-result , which officially should work on KMP, but has some issues.
- LelouBil 1y agoIn Kotlin, my go-to library is https://arrow-kt.io/ https://arrow-kt.io/
- greener_grass 1y agoResult is great but it ideally needs extensible union types (polymorphic variants) plus exhaustive pattern matching to work well.
- iandanforth 1y agoNot a fan. Code branches and this is Good Thing(TM). Result violates the single responsibility principle and tries to make what are distinct paths into a single thing. If your language has exceptions and returned values as distinct constructs then obfuscating them with Result means you end up fighting the language which becomes apparent fairly quickly. It's also a frustrating experience to want to interact with returned values directly and constantly have to deal with a polymorphic wrapper.
- mpalmer 1y agoResult violates the single responsibility principle and tries to make what are distinct paths into a single thing. You're trying to equate code paths (outcome handling) with values (outcomes), and that seems a more unfortunate category error.
- vjerancrnjak 1y agoI don't see how try-catch promotes single responsibility principle. I feel like this principle is just arbitrary. If I have Result<Error, Value> and I change the Error, I have to change all places that are using the Error type and tweak the error handling in mapLeft or flatMapLeft. If I instead raise Error and change it, I have to look at all the places where this error explodes and deal with it, not to mention, most languages won't even give me a compile time warning if I still keep the previous error type. I agree that if language does not have do-notation, that it's a bit ugly to sprinkle map and flatMap everywhere. Good example of ugliness is https://github.com/repeale/fp-go https://github.com/repeale/fp-go I think only an effect system, or a big environment object, places everything at 1 place, and when types change you have 1 place to edit the code. But starting immediately with an effect system (to abstract away control flow) or big env (to lift all ifs up) is premature.
- ATMLOTTOBEER 11mo agoYour complaint wrt sprinkling flatmap is the motivation behind scala for comprehensions
- Sl1mb0 1y ago
- tyleo 1y agoI always think more languages should support Result… but only to handle expected error states. For example, you may expect that some functions may time out or that a user may attempt an action with an invalid configuration (e.g., malformed JSON). Exceptions should be reserved for developer errors like edge cases that haven’t been considered or invalid bounds which mistakenly went unchecked.
- embedding-shape 1y agoI usually divide things in "errors" (which are really "invariant violations") and "exceptions". "exceptions", as the name implies, are exceptional, few and when they happen, they're usually my fault, "errors" on the other hand, depending on the user, happens a lot and usually surfaced to the user.
- tcfhgj 1y agowhy not divide things into errors and bugs (programming errors)?
- sanex 1y agoNot all exceptional circumstances are bugs
- embedding-shape 1y agoI'm currently working on something that requires a GPU with CUDA at runtime. If something went wrong while initializing the GPU, then that'd be an exceptuion/bug/"programming error" most likely. If the user somehow ended up sending data to the GPU that isn't compatible/couldn't be converted or whatever, then that'd be an user error, they could probably fix that themselves. But then for example if there is no GPU at all on the system, it's neither a "programming error" nor something the user could really do something about, but it is exceptional, and requires us to stop and not continue.
- deleted 1y ago[deleted]
- DarkNova6 1y agoThis is really just a syntactical issue. Not one of types or semantics. Non trivial operations have errors when the happy path fails. And with web apps IO can fail anytime, anywhere for any reasons. Sometimes you want to handle them locally, sometimes globally. The question is how ergonomic it is to handle this all for a variety of use cases. We keep reinventing the wheel because we insist that our own use cases are “special” and “unique”, but they really aren’t. Personally, I think Java’s proposal on catching errors in switches, next to ordinary data is the right step forward. Monads are great. You can do lots of great things in them, but ergonomic they are not. We should avoid polluting our type systems where possible.
- Thorrez 1y agofun register(registrationRequest: UserRegistrationRequest): UserDTO { return success(registrationRequest) .flatMap { validRequest -> throwIfExists(validRequest.email) { authService.userExists(validRequest.email) } }.flatMap { runWithSafety { authService.register(registrationRequest.email, registrationRequest.password) } }.getOrThrow() } There appears to be some useless code there. Why is validRequest.email being passed to throwIfExists twice? Why is throwIfExists implemented to return the email if the following line (runWithSafety) just ignores that returned email and instead gets the email from registrationRequest.email?
- jweir 1y agoAnd won’t the authService.register function also error if the user already exists? Or will it allow double registering the account? There are deeper problems here that a Result type is not gonna fix.
- adammarples 1y agoThe Authservice register function will error, it says so in the article
- Thorrez 1y agoThe imperative code has // log exception which doesn't exist in the Result version.
- DrakeDeaton 1y agoThere's certainly situations where this pattern creates some tricky ambiguity, but more often than not Result is quite an ergonomic pattern to work with. In case it's useful for anyone, here is a simple plug-in-play TypeScript version: ``` type Ok<T = void> = T extends undefined ? { ok: true; } : { ok: true; val: T; }; type Err<E extends ResultError = ResultError> = { ok: false; err: E; }; type Result<T = void, E = ResultError> = { ok: true; val: T; } | { ok: false; err: E | ResultError; }; class ResultError extends Error { override name = "ResultError" as const; context?: unknown; constructor (message: string, context?: unknown) { super(message); this.context = context; } } const ok = <T = void>(val?: T): Ok<T> => ({ ok: true, val: val, } as Ok<T>); const err = (errType: string, context: unknown = {}): Err<ResultError> => ({ err: new ResultError(errType, context), ok: false, }); ``` ``` const actionTaker = await op().then(ok).catch(err); if (result.ok) // handle error else // use result ``` I will be forever grateful to the developer first introduced to this pattern!
- bmn__ 1y agoThis doesn't pass the smell test. Whenever I've seen the Result or Either type, the definition looked different than what you wrote here. I doubt this composes nicely, with Folktale and fp-ts I can be certain.
- DrakeDeaton 11mo ago100% if you want composability go with a battle tested solution! That code really just demos how simple working the Result pattern can be.
- hotpotat 1y agoWith regard to AI, why not throw this whole article in an .md file and point CLAUDE.md to it? Codex is better at following rules so maybe you’d have more luck with that. But yeah, AI won’t code your way by default. People expect way too much out of the interns, they need direction.
- ActionHank 1y agoThis is one of the issues with LLMs in dev IMO. You either have the case that tech moves on and the LLM is out of date on anything new, so adoption slows or you have tech slowing down because it doesn't work with LLMs so innovation slows. Either way, it's great if you're working on legacy in known technologies, but anything new and you have issues. Can I write a spec or doc or add some context MCP? Sure, but these are bandaids.
- tasuki 1y ago> At the first glance, this code looks noisier and hard to understand Because of your inconsistent line-breaks!
- wiseowise 1y agoHow to rewrite boring, easily understood code into abomination. I'm not surprised to see Kotlin, for some reason there's a huge inferiority complex in Kotlin community where you have to write the most convoluted pseudo-fp code possible (not smart enough to use ML or Haskell, but still want to flex on Java noobs). I can't wait until they release rich errors and this nonsense with reinventing checked exceptions will finally end.
- taeric 1y agoIt is always funny to see that we try and force formulas to the early elementary shape that people learn. Despite the fact that chemistry, biology, physics, etc. all have advanced shapes for equations that do not have the same concerns. Similarly, when constructing physical things, it is not uncommon to have something with fewer inputs than outputs. Along with mode configured transfer of input to outputs.
- thefaux 1y agoI am not a fan of function chaining in the style advocated in the article. In my experience functional abstractions always add function call indirection (that may or may not be optimized by the compiler). You don't need a library implementation of fold (which can be used to implement map/flatmap/etc). Instead, it can be inlined as a tail recursive function (trf). This is better, in my opinion, because there is no function call indirection and the trf will have a name which is more clear than fold, reducing the need for inline comments or inference on the part of the programmer. I also am not a fan of a globally shared Result class. Ideally, a language has lightweight support for defining sum/union types and pattern matching on them. With Result, you are limited to one happy path and one error path. For many problems, there are multiple successful outputs or multiple failure modes and using Result forces unnecessary nesting which bloats both the code for unpacking and the runtime objects.
- anal_reactor 1y agoFunctional abstractions are great for writing code. They allow to nicely and concisely express ideas that otherwise take a lot of boilerplate. Now, for trying to debug said code... gl hf.
- valcron1000 1y ago} catch (exception: Exception) { // log exception throw exception } Probably the most harmful snippet ever written. Every blog post about errors has something similar written, regardless of the programming language. Please, don't write, suggest or even pretend that this should exist.
- another_twist 1y agoWhats the problem ? Is throwing directly better here ? Given the annotations I presume this is spring so it will probably be logged anyway. So maybe its unnecessary here.
- another_twist 1y agoI have had to write a Result abstraction multiple times. Honestly I think it should be part of standard java much like Optional.