[HN Gopher] Unwrapit provides a way to handle errors in JS/TS
       ___________________________________________________________________
        
       Unwrapit provides a way to handle errors in JS/TS
        
       Author : musicq
       Score  : 45 points
       Date   : 2023-10-31 05:29 UTC (17 hours ago)
        
 (HTM) web link (musicq.gitbook.io)
 (TXT) w3m dump (musicq.gitbook.io)
        
       | musicq wrote:
       | Spent sometime to complete the document of my rust Result like
       | library `unwrapit` for JS/TS.
       | 
       | Still think this might be a proper way to deal with errors in
       | JS/TS
        
         | optimistprime wrote:
         | This is interesting. Does it have any performance impact?
        
           | musicq wrote:
           | Don't think would affect performance. The core function
           | `wrap` is just try catch under the hood.
           | 
           | https://github.com/musicq/unwrapit/blob/d5c437a235ad1f5ad042.
           | ..
        
         | slawr1805 wrote:
         | This is awesome! I dig this when using Rust and am pumped to
         | try and sneak this in at work where we have a large TS
         | codebase.
        
           | jwhiles wrote:
           | This is a nice project, but it kind of feels like the result
           | of someone who's just learnt about Rust/Haskell and wants to
           | port what they know to JavaScript.
           | 
           | There's a lot of more established and battle tested libraries
           | with the same functionality - I've recently been using Purify
           | and finding it to be absolutely fine :)
           | 
           | their version of the same feature is here:
           | https://gigobyte.github.io/purify/adts/Maybe
        
             | grose wrote:
             | Whoa, these docs are really nice. Love how it includes the
             | functional Haskellish signature, TS signature, and useful
             | examples for each. Props to the authors.
        
         | fearface wrote:
         | 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.
         | 
         | 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.
        
       | quackware wrote:
       | Would be nice if this didn't have a dependency on rxjs just for a
       | single use of Observable
        
         | musicq wrote:
         | Actually rxjs is a `peerDependency` which means it will assume
         | the user will have it installed already.
        
           | chrisweekly wrote:
           | still a dependency
        
       | ikari_pl wrote:
       | 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.
        
         | musicq wrote:
         | Thank you for the suggestion! I will add that.
        
       | quickthrower2 wrote:
       | This is sort of saying "exceptions were a mistake", right?
        
         | qalmakka wrote:
         | 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!`.
        
       | mekster wrote:
       | 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.
        
       | satvikpendem wrote:
       | 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.
       | 
       | [0] https://www.effect.website/
        
         | Nezteb wrote:
         | A great introduction video on Effect:
         | https://www.youtube.com/watch?v=SloZE4i4Zfk
        
         | tentacleuno wrote:
         | 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.
        
           | chrisweekly wrote:
           | "recoiling at how much of a good idea it is"
           | 
           | "recoiling" implies a negative response, ie revulsion or fear
        
             | tentacleuno wrote:
             | Ahh, most likely meant to say recalling then.
        
       | oweiler wrote:
       | 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.
        
         | girvo wrote:
         | Indeed.
         | 
         | 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.
        
           | ghosty141 wrote:
           | I personally like the saying ,,when in rome, do as the romans
           | do". While you CAN retrofit almost anything to a lang, it
           | tends to be a constant fighting against the current which is
           | rarely worth it.
           | 
           | A good example is when working with Qt, trying to avoid
           | QString and QObject as much as possible is a fools errand,
           | its slower and ultimately ends up with ugly code.
        
             | sublinear wrote:
             | Try VTL
        
         | koenraad wrote:
         | 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.
        
           | Cthulhu_ wrote:
           | That's the big if, but even if your team agress to use this,
           | the greater JS ecosystem doesn't, so you end up being the odd
           | one out.
           | 
           | "Law of least surprise" is a good thing to keep in mind when
           | writing maintainable code.
        
         | tentacleuno wrote:
         | 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).
         | 
         | Then again, wrapping _everything_ is a huge pain.
        
         | musicq wrote:
         | 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.
         | 
         | https://musicq.gitbook.io/unwrapit/recipe/return-result-with...
        
       | joegaebel wrote:
       | Nicer way to go about this is `ts-results` which introduces the
       | `Result` type, inspired by rust.
       | 
       | https://github.com/vultix/ts-results
        
       | jondwillis wrote:
       | Is there a reason that typescript doesn't/can't add throwing
       | function annotation syntax?
        
         | eyelidlessness wrote:
         | 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.
        
           | jondwillis wrote:
           | Maybe I am misreading you but for example, in Swift, marking
           | a function as "throws" makes it a compiler error to not
           | handle the error, and makes it an error to not throw an error
           | or return in the function. In Typescript, the error in a
           | catch handler is of type unknown. I don't see how it could be
           | any worse than basically untyped errors AND unpredictable
           | runtime errors without reading the docs or implementation. At
           | least you would get some compiler help.
        
             | eyelidlessness wrote:
             | Here's an example of why it would be worse to have poor
             | error type coverage:                 function libFoo(foo:
             | string): throws FooError;            function libBar(bar:
             | number): boolean;
             | 
             | Both of these functions throw. With untyped errors, you
             | have to assume that this is at least a possibility. Now you
             | have every reason to assume it's safe to call one of them
             | without handling errors.
             | 
             | The argument as I understand it, which is convincing, is
             | that types like this are inevitable if typed errors were to
             | be introduced. The scope of even well typed code with no
             | documentation of even the errors thrown directly is
             | _enormous_. The scope of code which might propagate errors
             | from calls deeper in its stack is unimaginable. And there
             | is basically no way to do static analysis to meaningfully
             | reduce that space.
             | 
             | It could be fair to say this was a missed opportunity
             | earlier in TypeScript's development. But introducing typed
             | errors retroactively would lead, trivially, to millions of
             | cases like the above. Many in very hard to find ways.
             | 
             | Could those types be produced and refined to be as high
             | quality as existing types (first party or community
             | provided)? Of course. But it would take years. And unlike
             | introducing types where none exist, the gradual story just
             | doesn't exist. You'd have to treat literally every function
             | _and property access_ as `throws any` to get safety
             | equivalent to the gradual types story for non-errors.
             | 
             | And the incentive to type one's own code with errors is
             | hard to imagine: how can I, as a library author, say with
             | any confidence what anything I write throws, if I have no
             | idea what the underlying code throws (or if it really even
             | does)? Why would I take on the maintenance burden of
             | getting it wrong? Or incomplete? Or incomplete _and wrong_?
        
         | tgv wrote:
         | 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.
        
           | jondwillis wrote:
           | Why can't provenance of throws be traced with certainty? It
           | is possible to trace the return type of functions.
           | 
           | Even if it were an opt-in/progressive adoption keyword like
           | "throws" that enforced some compiler guardrails, I think that
           | could be a huge win for reducing unexpected runtime
           | exceptions.
        
             | tgv wrote:
             | Take the following function:                   function
             | arrmin<T extends {field: number}>(a: T[]): number {
             | let m = a[0].field;             for (let i = 1; i <
             | a.length; i++) {                 if (a[i].field < m) {
             | m = a[i].field;                 }             }
             | return m;         }
             | 
             | Now call arrmin([]). There's no way to avoid a throw. You
             | would have to annotate every function that uses field
             | selection on an array element (and a bunch of other
             | conditions) as "throws", or enforce error checking code (if
             | (0 <= i < a.length) {...}).
             | 
             | Rust didn't solve that either, does it? It just panics.
        
         | uallo wrote:
         | https://github.com/microsoft/TypeScript/issues/13219
        
       | trungdq88 wrote:
       | 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".
        
         | redact207 wrote:
         | I think their reasoning is that it's type safe so the compiler
         | complains that you didn't handle something that returns an
         | error.
        
       | pyrolistical wrote:
       | So a monad.
       | 
       | 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
        
       | chmod775 wrote:
       | 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.
        
         | tentacleuno wrote:
         | 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.)
        
           | chmod775 wrote:
           | There's a fair chance modern JS engine's Promise
           | implementations are going to beat a custom wrapper function
           | that is also wrapping everything, contains a try/catch
           | statement, multiple typeofs, and creates an instance of a
           | class for every result.
           | 
           | Not that it matters - if you're going down this route either
           | way, performance is not your priority. If you really care
           | about performance, don't use either if you don't have to.
        
             | tentacleuno wrote:
             | No, the whole _point_ of Promise implementations is _not_
             | to beat tasks which run in the current event loop tick.
             | That 's why you have separate (microtask, macrotask etc.)
             | queues in the first place.
             | 
             | Unnecessarily using Promises introduces further work for
             | the VM, plus it may cause issues with race conditions,
             | wrong variable values (which aren't easy to debug), etc. if
             | you don't hold a magnifying glass to your code.
             | 
             | To be clear, I'm not benchmarking OP's library -- just
             | providing an important note on why you shouldn't use
             | Promise as a general monad. There are libraries for that:
             | maybe not this one, maybe nothing at all. Do whatever works
             | for you :-)
        
               | chmod775 wrote:
               | > No, the whole point of Promise implementations is not
               | to beat tasks which run in the current event loop tick.
               | That's why you have separate (microtask, macrotask etc.)
               | queues in the first place.
               | 
               | We're clearly talking about actual CPU time spent here
               | when comparing code using those approaches. We're not
               | interested in what runs first in some software mixing
               | synchronous and asynchronous code. This is completely
               | irrelevant.
        
               | tentacleuno wrote:
               | > We're clearly talking about actual CPU time spent here
               | when comparing code using those approaches. This is
               | completely irrelevant.
               | 
               | Perhaps I missed a part of the conversation? I began by
               | highlighting the issues with your approach regarding the
               | event loop, and you started talking about the
               | implementation of this library in particular, seemingly
               | as a counter-argument?
               | 
               | To be clear, I'm not disagreeing that wrapping things is
               | slower than not wrapping them. That's quite obviously
               | true. I'm just pointing out bad advice in your parent
               | comment.
        
         | the_gipsy wrote:
         | That would make everything async which is not what you want.
        
       | meowtimemania wrote:
       | I'd prefer an api like this:
       | 
       | const [result, error] = attempt(() => someFunc());
       | 
       | This way you don't have to wrap all your functions.
        
         | tentacleuno wrote:
         | That seems a lot more ergonomic.
         | 
         | I can already see someone copy-pasting `wrap(myFunction)(args)`
         | everywhere :-)
        
           | musicq wrote:
           | I think the most usual way in a project is to use this style.
           | Wrap is a simple way to let you get Result type without
           | refactoring your implementation.
           | 
           | https://musicq.gitbook.io/unwrapit/recipe/return-result-
           | with...
        
             | meowtimemania wrote:
             | Then I'm tightly coupling all my code to this library. I
             | would prefer to write javascript like normal and not import
             | a library anytime I author a function.
        
         | vmfunction wrote:
         | have you look at ts-fp, it is ts not js, but still.
        
       | randomdev3 wrote:
       | 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.
        
         | thiht wrote:
         | 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.
        
           | mekster wrote:
           | You sound like, living in a car society is a nightmare
           | because you can't dodge cars that run into you.
           | 
           | Hopefully there's an ideal world for you.
        
             | thiht wrote:
             | > living in a car society is a nightmare
             | 
             | More like living in a society with invisible cars. You only
             | know there was a car when it hits you. And every time you
             | move a foot you have to first try to throw a pebble to
             | check if there's a car
        
         | musicq wrote:
         | 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.
        
         | golergka wrote:
         | 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.
         | 
         | Personally though I would go with fp-ts.
        
       | janpot wrote:
       | Feels like there could be a place for a Promise.settled method
       | that mimics Promise.allSettled, but for a single value.
        
       | solumunus wrote:
       | 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.
        
         | tentacleuno wrote:
         | 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.
        
       | rswskg wrote:
       | Will throw this in here as it's been useful for errors if
       | operating with try/catch
        
         | musicq wrote:
         | 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.
        
       | theogravity wrote:
       | I'm not sure I agree with this approach, but I'd add instructions
       | on how you would do unit testing when using this library.
        
       | tills13 wrote:
       | My manager likes to ask "is it better or is it just different"
       | 
       | Well... It's certainly different.
        
       ___________________________________________________________________
       (page generated 2023-10-31 23:01 UTC)