Hacker News new | past | comments | ask | show | jobs | submit login
Unwrapit provides a way to handle errors in JS/TS (gitbook.io)
50 points by musicq 12 months ago | hide | past | favorite | 70 comments



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

Then again, wrapping everything is a huge pain.


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.


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.


Try VTL


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...


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.


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.


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.

Personally though I would go with fp-ts.


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.


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.


> 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


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/


A great introduction video on Effect: https://www.youtube.com/watch?v=SloZE4i4Zfk


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.


"recoiling at how much of a good idea it is"

"recoiling" implies a negative response, ie revulsion or fear


Ahh, most likely meant to say recalling then.


Nicer way to go about this is `ts-results` which introduces the `Result` type, inspired by rust.

https://github.com/vultix/ts-results


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.


Thank you for the suggestion! I will add that.


Thank you!


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.


Would be nice if this didn't have a dependency on rxjs just for a single use of Observable


You might like https://github.com/vultix/ts-results -- no dependencies. (Beware maintainer is not active, https://github.com/vultix/ts-results/issues/37)


Actually rxjs is a `peerDependency` which means it will assume the user will have it installed already.


still a dependency


I'd prefer an api like this:

const [result, error] = attempt(() => someFunc());

This way you don't have to wrap all your functions.


That seems a lot more ergonomic.

I can already see someone copy-pasting `wrap(myFunction)(args)` everywhere :-)


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...


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.


have you look at ts-fp, it is ts not js, but still.


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 think their reasoning is that it's type safe so the compiler complains that you didn't handle something that returns an error.


Is there a reason that typescript doesn’t/can’t add throwing function annotation syntax?


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.


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.


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.


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.


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.


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?



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


This is sort of saying "exceptions were a mistake", right?


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!`.


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


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.


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.


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


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.


This is interesting. Does it have any performance impact?


Don't think would affect performance. The core function `wrap` is just try catch under the hood.

https://github.com/musicq/unwrapit/blob/d5c437a235ad1f5ad042...


iirc, try catch does have performance impact in js.


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


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.


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


> 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.


> 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.


That would make everything async which is not what you want.


My manager likes to ask "is it better or is it just different"

Well... It's certainly different.


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.


Feels like there could be a place for a Promise.settled method that mimics Promise.allSettled, but for a single value.


Will throw this in here as it's been useful for errors if operating with try/catch



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.


.catch




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: