I've come to the conclusion that such a library offers little benefit in languages which embrace exceptions.
You have to basically wrap everything, making the code hard to read, and without proper ADTs and pattern matching it's easy to forget handling all cases.
There's also no way to prevent ppl from falling back to exceptions, so now you have a mess of exception and result handling.
And bc there is no blessed way, bigger projects typically include multiple different, incompatible result types.
One of the advantages of libraries such as these (in my opinion) is it gives you the ability to incorporate the errors thrown into the return type of functions, creating a broader contract than you would ordinarily achieve.
This is mainly in the context of TypeScript (think IntelliSense, etc.), also considering the fact that TypeScript doesn't support typed errors (neither does Flow, FWICS).
A fun example of this is Nim: “results” is quite a nice library, but Nim’s stdlib and by extension most of its third party libraries are built around exceptions (or std/options).
And Nim uses effect tracking, you can encode the exceptions raised into the type signature, so in some ways it’s perfectly poised for something like “results” or unwrapit
Yet when you try to go down that path (like we did at work, doing embedded firmware) it feels like you’re fighting against the current, sadly.
This library provides a way to encapsulate errors into a Result type, so that any user want to retrieve the value, they must unwrap the Result first. In this way I think it will force people to realize that there will be errors, you need to handle them. For JS users, you need to call _result.value_, for TS, the editor and compiler will warn you.
In my opinion, it never tries to prevent people from handling exceptions using try/catch, instead, it enrichs the error handling solutions.
`wrap` function is just a simple way for users to adopt this Result type quickly. I think in projects, developers should create their own Result usually, can check this example.
You are talking about a larger fundamental problem; code quality.
This library can be a way to handle errors just as the try/catch block is. If agreed on within the team ofcourse.
Why make it complicated? The language supports catching errors, use that. You may not like it but that's the thing you have. Of course you can wrap errors, return [response,error] or whatever in your implementation of api calls etc. but you don't need third-party libraries for that.
The thing is not using try/catch, like the first case, sometimes you don't know if a function will throw or not. You can't tell from the function signature as well.
So this provides a way that told you the function you called might throw. It's more like an alert before a crash.
[res, err] is good, actually I used this style for a long time. `unwrapit` is a nicer way to let you write [res, err], like type hints and other utility methods.
Because errors have different semantics. Some should kill your whole app and propagate up to the top layer, where a transaction that wraps the whole http request will be cancelled, or a error screen will be shown to the user. But other errors are part of business as usual — they should be handled explicitly, and type system should force you to do it and match the right exception type.
Agree with that. Exceptions suck, try-catch is an infamy, and errors not in the signatures or exploitable in any way by Typescript is a living nightmare. But what I hate even more is multiple ways to handle errors.
Just use a library that already contains this and more functional programming idioms, like fp-ts or its successor, Effect [0]. It is a little more complex to learn but much more robust that simply implementing your own Result and other types.
Effect looks great. Strangely enough, I recall dreaming of it and recoiling at how much of a good idea it is -- can't wait to give it a proper go in a project.
For anyone who has NOT written in rust yet, there's nothing in the readme that explains what's intuitive or easy about the library. It doesn't even explain what to expect as the `status` getting logged, not go mention other possible values.
Why not expect that something would throw at some point where app logics are taking place and wrap with try-catch at your lower layer and handle uncaught errors there?
Adding all these boilerplate that not all people agree with would just make it harder to read and debug.
In certain projects I can see why you would reach for functional style error handling, but for the vast majority of standard web apps/apis (what people are typically building in JS/TS) try/catch is far superior. Have the error bubble up to your endpoint, serve either a specific error message or a vague error message depending on error type, log accordingly. This is so simple, much quicker to develop and provides all the functionality you need. You can still use returned errors in the places that really call for it, but in my experience those instances are in the minority.
Ironically, I bet that a lot of React-driven apps use try/catch on a regular basis. Despite not doing a deep dive into the topic, I believe React's "asynchronous" (Suspense?) functional components actually throw Promises, and the runtime awaits them before re-rendering the component.
Maybe I'm old, but I can't see any reasons why this is better after looking at the before/after example. This is adding unnecessary complexity with very little benefit (if at all).
If anything, the "try...catch" example actually look clearer and better than "!user.ok".
I would guess it is because just about every function can throw, and TS can't detect it with certainty, even for functions which are 100% TS. It also doesn't improve transcription or performance.
From memory: even if the arguments in favor were thoroughly convincing, it’s basically an insurmountable task to produce the types to cover even common runtimes, let alone the vast ecosystem of libraries (often themselves with community-maintained types). The failure mode for poor coverage of error types is worse than the failure mode of untyped errors, for instance because it would tend to influence where developers focus their error handling efforts in misleading ways.
Btw zig also does this in a nice way IMO. You declare possible error enums and the returned value is a union of all the errors or the success type. You then use existing union handling at each call site
Exceptions are undoubtedly a mistake, because error handling is to important to untie errors completely from the code that generates them. That doesn't mean that stack unwinding doesn't have its uses.
For instance I find very convenient to use C++'s exceptions for truly exceptional cases - those in which I'd like the software to quit but I don't think aborting is a good solution (because maybe I want to properly clean up the application's state, etc). That's why Rust for instance uses C++ stack unwinding for `panic!`.
I think the idea of try/catch is to let the error bubble up to the place where it can be handled. It usually results into having error handling in a few central places. Your example in github is IMO not how to make best use of try/catch.
Promises already are a wrapper that can contain an error or a value, and using 'await' is basically unwrapping it.
This library adds very little value and is just another layer of abstraction which removes the syntactic sugar that was added with the previous layer and tries to re-implement stuff we already have (like responding to uncaught errors).
Just use base promises and .then/.catch etc if you want to deal with values/errors this way. Don't introduce another dependency that does almost nothing, and which others reading your code will have to familiarize themselves with.
I get that this makes doing some things slightly more ergonomic and less verbose, but at the end of the day it saves you seconds while costing others who have to look up documentation/code of yet another library much more time. Not to mention yet another dependency with sub-dependencies you have to manage. It wants specifically rxjs ^7.8.1 despite only using stable parts of that API.
I probably just spent more time evaluating this thing than it ever would have saved me.
Promises are inherently going to be a lot slower than synchronous code (not to mention timing implications, re: the event loop, microtask queue, etc.). As such, wrapping everything in Promises is not a good idea -- reserve it for actually asynchronous operations.
(All of that's not even to mention the confusion around what operations in the code actually perform asynchronous actions.)
try/catch has no problem, but the premise is that you know there will be errors been thrown. Say a function `divide`, you can barely tell that whether it will throw errors or not without looking into the source code.
[+] [-] oweiler|2 years ago|reply
You have to basically wrap everything, making the code hard to read, and without proper ADTs and pattern matching it's easy to forget handling all cases.
There's also no way to prevent ppl from falling back to exceptions, so now you have a mess of exception and result handling.
And bc there is no blessed way, bigger projects typically include multiple different, incompatible result types.
[+] [-] tentacleuno|2 years ago|reply
This is mainly in the context of TypeScript (think IntelliSense, etc.), also considering the fact that TypeScript doesn't support typed errors (neither does Flow, FWICS).
Then again, wrapping everything is a huge pain.
[+] [-] girvo|2 years ago|reply
A fun example of this is Nim: “results” is quite a nice library, but Nim’s stdlib and by extension most of its third party libraries are built around exceptions (or std/options).
And Nim uses effect tracking, you can encode the exceptions raised into the type signature, so in some ways it’s perfectly poised for something like “results” or unwrapit
Yet when you try to go down that path (like we did at work, doing embedded firmware) it feels like you’re fighting against the current, sadly.
[+] [-] musicq|2 years ago|reply
In my opinion, it never tries to prevent people from handling exceptions using try/catch, instead, it enrichs the error handling solutions.
`wrap` function is just a simple way for users to adopt this Result type quickly. I think in projects, developers should create their own Result usually, can check this example.
https://musicq.gitbook.io/unwrapit/recipe/return-result-with...
[+] [-] koenraad|2 years ago|reply
[+] [-] randomdev3|2 years ago|reply
[+] [-] musicq|2 years ago|reply
So this provides a way that told you the function you called might throw. It's more like an alert before a crash.
[res, err] is good, actually I used this style for a long time. `unwrapit` is a nicer way to let you write [res, err], like type hints and other utility methods.
[+] [-] golergka|2 years ago|reply
Personally though I would go with fp-ts.
[+] [-] thiht|2 years ago|reply
[+] [-] satvikpendem|2 years ago|reply
[0] https://www.effect.website/
[+] [-] Nezteb|2 years ago|reply
[+] [-] tentacleuno|2 years ago|reply
[+] [-] joegaebel|2 years ago|reply
https://github.com/vultix/ts-results
[+] [-] ikari_pl|2 years ago|reply
[+] [-] musicq|2 years ago|reply
[+] [-] mekster|2 years ago|reply
Adding all these boilerplate that not all people agree with would just make it harder to read and debug.
[+] [-] solumunus|2 years ago|reply
[+] [-] tentacleuno|2 years ago|reply
[+] [-] quackware|2 years ago|reply
[+] [-] yencabulator|2 years ago|reply
[+] [-] musicq|2 years ago|reply
[+] [-] meowtimemania|2 years ago|reply
const [result, error] = attempt(() => someFunc());
This way you don't have to wrap all your functions.
[+] [-] tentacleuno|2 years ago|reply
I can already see someone copy-pasting `wrap(myFunction)(args)` everywhere :-)
[+] [-] vmfunction|2 years ago|reply
[+] [-] trungdq88|2 years ago|reply
If anything, the "try...catch" example actually look clearer and better than "!user.ok".
[+] [-] redact207|2 years ago|reply
[+] [-] jondwillis|2 years ago|reply
[+] [-] tgv|2 years ago|reply
[+] [-] eyelidlessness|2 years ago|reply
[+] [-] uallo|2 years ago|reply
[+] [-] pyrolistical|2 years ago|reply
Btw zig also does this in a nice way IMO. You declare possible error enums and the returned value is a union of all the errors or the success type. You then use existing union handling at each call site
[+] [-] quickthrower2|2 years ago|reply
[+] [-] qalmakka|2 years ago|reply
For instance I find very convenient to use C++'s exceptions for truly exceptional cases - those in which I'd like the software to quit but I don't think aborting is a good solution (because maybe I want to properly clean up the application's state, etc). That's why Rust for instance uses C++ stack unwinding for `panic!`.
[+] [-] musicq|2 years ago|reply
Still think this might be a proper way to deal with errors in JS/TS
[+] [-] fearface|2 years ago|reply
Here’s lots of typical exception handling patterns: http://wiki.c2.com/?ExceptionPatterns
Persobally I prefer exceptions over boilerplate “if’s”, but good to know that there’s a wrapper for the people who don’t.
[+] [-] slawr1805|2 years ago|reply
[+] [-] optimistprime|2 years ago|reply
[+] [-] chmod775|2 years ago|reply
This library adds very little value and is just another layer of abstraction which removes the syntactic sugar that was added with the previous layer and tries to re-implement stuff we already have (like responding to uncaught errors).
Just use base promises and .then/.catch etc if you want to deal with values/errors this way. Don't introduce another dependency that does almost nothing, and which others reading your code will have to familiarize themselves with.
I get that this makes doing some things slightly more ergonomic and less verbose, but at the end of the day it saves you seconds while costing others who have to look up documentation/code of yet another library much more time. Not to mention yet another dependency with sub-dependencies you have to manage. It wants specifically rxjs ^7.8.1 despite only using stable parts of that API.
I probably just spent more time evaluating this thing than it ever would have saved me.
[+] [-] tentacleuno|2 years ago|reply
(All of that's not even to mention the confusion around what operations in the code actually perform asynchronous actions.)
[+] [-] the_gipsy|2 years ago|reply
[+] [-] tills13|2 years ago|reply
Well... It's certainly different.
[+] [-] theogravity|2 years ago|reply
[+] [-] janpot|2 years ago|reply
[+] [-] rswskg|2 years ago|reply
[+] [-] rswskg|2 years ago|reply
[+] [-] musicq|2 years ago|reply
[+] [-] nsonha|2 years ago|reply