[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)