Hacker News new | past | comments | ask | show | jobs | submit login
From Rust to TypeScript (valand.dev)
213 points by valand on Sept 12, 2020 | hide | past | favorite | 153 comments



The first code example he uses immediately threw me for a loop. Why is he adding the return value of a function call to a function itself? bar() + bar????

function bar(someNumber: number)

{

  return foo(someNumber) + 1;  
}

function baz(someNumber: number) {

  return bar(someNumber) + bar; 
}

baz();

Moreover, baz is called with its only argument being undefined-- so the exception throwing code won't even throw, because it isn't 0. And _moreover_, this is typescript, so this would have thrown a type error since a required numeric argument is omitted.

This has so many problems. This is not a promising start to this article's correctness or thought-outtedness. Yet here it is at the top of hacker news...


Author here. Thanks for the correction.

You caught me red-handed for not testing these codes in playground.


In JS it's the function would be converted to a string representation of its code and concatenated to the result of `bar(someNumber)`.

Some quick playing with ts-node says that the same thing should happen, depending on the return type of bar. Given the definitions above, it should throw a TypeError, but if bar returned a string, I think it would still behave as I said.


I was unsure if this comment was sarcasm so I tried and concatenating a function and a string in JS actually results in the source of the function being concatenated with the string.


<rant> This whole "wow I thought implicit type coercion in JS was a joke" style comment got old 10 years ago. If you look at where JS came from (the 90s browser) and what it tried to do there then you should be left with few surprises looking it JS code snippets. If you don't understand what a language was designed for and how it evolved how can you be surprised at the results of code snippets? Because it's not the same as every other language? What would the point of the new language be then?</rant>


It’s not that the language is different, it’s that the default is stupid. It’s much more likely that if I’m trying to concatenate a string and a function, I’ve mistyped something, not that I want the source code of the function.

You can always include some additional function which takes a function as an argument and prints the source code as a string for the 4 people in the world who rely on function to string coercion, why is it a default behavior with no visibility?

I get it, JS is different from C or C++ and every other language but we call out poor design decisions by comparing them to expectations fostered by other languages


Again how can you say it's an outrageous default if all you reference is what you think it should have done from a generic language PoV not looking to understand why it does it that way?

Javascript came in as a scripting language for working with the untyped string content of a browser so you get dynamic duck typing where it tries to make the types work (generally towards string since that's what the browser had) instead of having the scripter trying to make some simple web content dynamic do more type checks than actual code. In JS functions are just objects (like most everything) and objects. Combine the above so when you say object + "foo" it coerces object to string and it shouldn't seem outrageous just not what you're used to.

Now a fair question would be "Why doesn't TS consider this an error out of the box".

Also for those annoyed by this in places JS wasn't originally designed to go (heavy app logic, server backends, desktop applications) you might want to look at the no-implicit-coercion rule in ESLint or similar.


I have been coding TypeScript for the last 2 years, non-stop, which also shielded me from the surprise of the function's body getting stringified.

It's relatable. Most developers did not grow up with the knowledge of JS language's Design principles, which is not something I would criticize anyone for.


Plus the undefined name 'number' in foo(). TypeScript flags that along with 'baz();'.

What does puzzle me is that TS doesn't flag 'return bar(someNumber) + bar;' - I'm curious if anyone knows why it seems to allow that.


I think because the return type hasn't been specified for the function, so it is inferring the return type from the return statement (which means it is "correct").

Don't forget JavaScript (and therefore typescript) will let you concatenate a lot of random types together without error.


Sounds like you posted this comment after only reading that example at the beginning. Did you read the rest of the article? What were your thoughts at the end of it?


Correct. There's about 19 billion opinionated articles on the internet. If I start one off and it seems immediately wrong, I'm not going to take the time to read the entire essay. You have to have some kind of filtering heuristic for content.


You'll take the time to leave a comment, though. And engage in follow-up discussion, apparently.

It's not a long read and it has some useful content (that I mostly agree with) despite having a bunch of minor mistakes. I noticed them too, but instead of loudly dismissing the entire thing on a public forum my first instinct was to look for a repo so I could send a pull request (sadly I didn't find one).


I love this. It reminds me a lot of the work I did in Prism to port what I learned using Haskell in TypeScript: https://dev.to/vncz/forewords-and-domain-model-1p13

Reading the article, I do have some suggestion:

1. Never use isLeft or isRight; they break the composability of the monad instance — use `fold` instead, as late as possible

2. Do not use Enum in TypeScript — they have a runtime representation in JavaScript that's likely messing up a lot of things. Use string unions to accomplish the same result


I really wish the TS team would have used frozen null prototype objects for enums instead. Feels like it would have been a perfect fit.

With that said I am curious why you think one shouldn’t use enums. I know it’s a “factory” IIFE like namespaces at runtime but otherwise I don’t understand why it’s undesirable?


My main issue with Enums in Typescript:

1. Because they have a runtime representation, I cannot do `import type` — and I am importing the entire file (which might have a side effect). While it's still not justified (ideally importing should not do anything) the reality in JavaScript is different.

2. I can swap the enum value with a number or a string and it would still work, even if invalid. See https://www.typescriptlang.org/play?#code/KYOwrgtgBAggxgFwJY... for an example

3. On the other hand, I cannot use strings to create an Enum (https://www.typescriptlang.org/play?#code/KYOwrgtgBAygLgJwJY...) — this is the exact opposite of n. 2

4. Duplicates are possible: https://www.typescriptlang.org/play?#code/KYOwrgtgBAIghgTwPI...


1. You can define enum like `const enum A {...}` and they will not have runtime representation.


Correct, but that does not solve the other issues I pointed.

Additionally, const enums DO NOT have an associated type. That is a problem in libraries, since users cannot reuse the type definition around (it back ports to a string or a number)


const enum doesn't work when in transpileOnly mode (e.g. webpack, Parcel), and Babel treats "const enum" the same as "enum". I'd really love a non-broken const enum in TypeScript, though.


I'd love to see them out of the language. I do not have evidence/document supporting this, but I think it was ultimately a failed attempt of copying the C# feature ignoring the fact that JavaScript works differently.


Why is #3 a problem? It's a feature! Enums are a great way to enforce values to a predefined set. I don't want you to work around it with random strings. Use the enum. That's what it is there for.


The biggest issue I have with enums is that unlikely virtually everything else in TypeScript, they are a "thing" (you correctly mentioned in your next comment that they don't have to be depending on their declaration, but that nuance will be lost on 99% of users). They're not just "types on top of JS", which defeats a lot of expectations in TypeScript. They're also not standard JS, so best practices with them aren't nearly as studied.

If they were to be part of the standard, it would be another story.


Yes. There are so few TS features that are non-type-level and non-standard. One option is to lint against these few features and call it a day.

  {
    "rules": {
      "no-restricted-syntax": [
        "error",
        {
          "selector": "TSEnumDeclaration",
          "message": "Enum restricted."
        },
        {
          "selector": "ClassDeclaration[abstract=true]",
          "message": "Abstract class restricted."
        },
        {
          "selector": "TSParameterProperty",
          "message": "Parameter property restricted."
        }
      ]
    }
  }
(TS decorators are already marked experimental)


There are also const enums, which get inlined/erased at compile time. Even so, I still avoid using enums and prefer literal types instead.


I don’t understand why so many people advise against TS enums. The string variant is no different at runtime from a const of the same mapping, but they also achieve nominal typing in an otherwise structural type system, which is super useful for preventing ambiguity errors


I've done TypeScript for 5-6 years, in up to 100+k loc codebases, and never had a problem with enums, both with tsc and babel (so yeah, `const enum` no worky).

`Object.values(SomeEnum)` is pretty handy, something you can't do with string union types.


See my response above!


1.

Ah thanks for the suggestions. These reminds me scrounging this article https://dev.to/gcanti/getting-started-with-fp-ts-either-vs-v...

My pet peeve doing this in JS/TS is the fact that writing

``` fold( () => someIdealVal, () => someNonIdealVal )(someAssertion(someValue)); ```

means that those functions passed as the parameters might be instantiated at the execution time rather than on compile time. (This is not proven as the final resulting machine code depends on how good the bundler(webpack)/compiler(tsc)/runtime(v8/rhino) is)

For the sake of performance, I am tempted to write it like this

``` const a = () => someIdealVal const b = () => someNonIdealVal

const someFold = fold(a, b); ```

Writing it like this feels worse for readability. The "story of the function" cannot be told top to bottom, therefore other devs who read this must jump between lines non-sequentially like watching Christopher Nolan's memento.

Rather than pitting readability against performance, I prefer to shelve that fold/pipe/chain pattern for later consideration

2.

I can relate to this. Enums are frustrating especially if you have to write library with it.

I have a pretty good time using this alternative instead. https://medium.com/@martin_hotell/10-typescript-pro-tips-pat... this article point number 4


If I'm understanding you correctly, then fp-ts has the `constant` function to enable what you're describing in #1:

    fold(constant('my static fallback'), f)(x)
What sucks about this, as much as it's my preferred pattern, is that without native pattern matching in the language it's unclear which callbacks are for which type constructors. It's not so bad for something like Either, but if you're using something like DatumEither with ~five constructors it's a bit of a mess.


This article is unconvincing.

From the article: Compared to the first snippet, this one is harder to debug. It seems so because 1.) You don't know that callMe will throw an error or not. 2.) If it throws an exception you will not know what is the type of the exception. The more modular your code is, the harder it is to debug exceptions.

This is only a problem with unchecked exceptions as seen in TypeScript and C#. Checked exceptions are the solution.

The problem with error code return is tediousness:

  int foo() {
     int result = bar();
     if (result == ERROR_CODE)
        return result;
     int result2 = baz();
     if (result2 == ERROR_CODE)
        return result2;
     int result3 = bazz();
     if (result3 == ERROR_CODE)
        return result3;
  }
Compare to the same code written in a language that supports checked exceptions:

  int foo() throws SomeException {
     int result = bar();
     int result2 = baz();
     int result3 = bazz();
  }
In the first version the logic is interspersed with error handling, which makes the logic hard to see.

How do people using Go and other languages that don't have exceptions solve this reduced readability problem?


The article doesn't get into it, but Rust has an error-handling operator, `?`, that encapsulates the branch-and-return-on-error check. Your code in Rust would look like:

    fn foo() -> SomeResult {
        let result = bar()?;
        let result2 = baz()?;
        let result3 = bazz()?;
    }
The fundamental difference between Rust's approach and Java-style checked exceptions arises when those three functions have different error types. In Java, you would annotate the function with every possible exception that the function can throw. In Rust, a library (or module within a library) would normally design its own Result type to encompass every error that the library/module can throw, and use that type on every error-producing function (you could reproduce the Java approach by defining a new Result type for each function, but I've never seen anyone bother with that). The end result is that you avoid the precise bookkeeping that so annoys people about Java's checked exceptions, but you also tend to lose some precision with reading the code: from reading the type signature you know that a function with a custom error type has the potential to error, which is useful, and you know that the error must be at least one of the types in the error's definition, but it might not exhibit all of the error types that the custom error type can contain. Think of it like a spectrum: from not having any indication that that a function may error (e.g. JavaScript), to knowing that a function may error but not having any indication of how (Swift), to knowing a function may error with at least one of a known set of types (Rust), to knowing that a function may error with an exactly known set of types (Java with checked exceptions).


We define a custom error type for most functions in the Rust codebase I’m currently working on. It’s a little laborious but not that big a deal and I really enjoy being on the “completely precise” side of the spectrum :).

It still feels lighter weight than Java’s checked exceptions and I think maybe that comes down to all the language machinery that Rust offers around patten matching and error combinators.

I plan to blog about our error handling strategy... someday


Interesting. Although you're the first person I've heard about actually taking that approach, I have heard people lament that Rust library error types tend to be maximal rather than precise (IOW, peeved that a function's declared error type contains errors that that specific function can't actually produce; this is usually a jumping-off point to a spiel about asking Rust to support anonymous sum types), so I'm not completely surprised that somebody out there is actually doing it. That said, I've also heard people on the other side of the spectrum express that Swift's "I don't really care about the specific error type" is what they prefer; notably the anyhow crate (https://docs.rs/anyhow/1.0.32/anyhow/) is a good way to get that behavior in Rust while remaining idiomatic.


I think it is possible to automate some of our approach with a simple macro, I just haven't gotten to it.

I really like that you can look at a function and know every single type of exit it can have and know that you've handled them all.

But this might be because the thing that we are building (replicache.dev) runs in someone else's process so we need to be good citizens and be very careful to never panic or return unexpected/undocumented errors.


> the precise bookkeeping that so annoys people about Java's checked exceptions

This just came up in a different context (https://news.ycombinator.com/item?id=24448865) but I don't think it's the precise bookkeeping that annoys people about Java exceptions so much as the fact that you often can't be all that precise and yet you still need to do a lot of bookkeeping.


Thank you for the excellent comparison with error precision. I’m diving into Swift (Vapor) and was curious if this remains true with Swift 5.2?


I'm no Swift expert, but I don't believe that any changes have been made to its idiomatic error-handling approach. Using the first example from the Swift 5.3 docs ( https://docs.swift.org/swift-book/LanguageGuide/ErrorHandlin... ):

    func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
        let snackName = favoriteSnacks[person] ?? "Candy Bar"
        try vendingMachine.vend(itemNamed: snackName)
    }
The `try` is analogous to Rust's `?`, and the `throws` is the part of the type signature that indicates that the function may error but without any additional information about how it may error. Of course, I imagine a capable IDE should still be able to inspect all the `try`s in a function to determine what types can possibly be thrown.


Yes, that is still the case with 5.2. However, a proposal to add typed `throws` is currently being drafted and prototyped [0]. It won't be available in Swift 5.3, but if all goes well it might land in the next release after.

(Also, if you're doing Vapor programming, you may be pleased to know that Swift 6 is expected [1] to bring a real concurrency model, which will likely include some form of async/await so that you won't have to muck about with all those nested closures.)

[0]: https://forums.swift.org/t/typed-throws/39660

[1]: https://forums.swift.org/t/on-the-road-to-swift-6/32862/


Go does indeed use the tedious approach. Rust and Zig have nicer syntax for it.

But personally, I prefer even Go's "tedious" approach to (unchecked) exceptions, simply because it's too easy to miss an error case otherwise.

I also find that knowing where errors occur is important information, so I don't really mind having it add an extra line or two. (But the more minimal Rust/Zig syntax is nicer.)

(This attitude comes from having once worked on a python server application, which involved a lot, "oh shit, that throws" whackamole.)


Go's approach is not great. You can leave off return assignment completely. You can't take a tuple as an argument. It has an amateur approach to how you can't shadow `err`, yet `:=` vs `=` change on whether `err` already exists or if your lhs variable is new. You have to do additional work to get a stack trace, errors are usually just strings. You may be forced to handle some errors (unless you trivially ignore them), but nobody is forced to make decent errors. Most libs don't even think about them, like being forced to handle `throw "something went wrong"`.

I like errors as return values (mainly Result<V, E>), but Go's is probably the worst impl I've come across. The reason why it's still productive despite this is because we, as developers, tend to not put much thought in errors anyways, so who cares if it sucks (until we need them).


The lack of a tuple type is the most frustrating for me, because it means you cant define channels as a type of tuple. You have to go through the boilerplate of defining an option type for every single T channel that you handle.


Couldn’t you just use a pointer?


When I say 'option type' I actually mean the idiomatic go multiple return `(res, err)`. You can define a function to return that but its not actually a first class T, so you cant define a channel that follows the same semantics. That means if you have a bunch of functions that use the `(res, err)` style and you later want to convert them to, e.g., batch processing steps using channels, you have to now add a lot of boilerplate type definitions to tie things together.


Ah got it. Tuples come with their own challenges and are quite similar to structs, so I’m not surprised they didn’t make the cut. I’ve had to define structs like you say occasionally, but it literally takes like 2 seconds so I don’t really care, and appreciate not having to decide when to use tuples vs structs and argue with teammates about it too.


Youre right, it definitely isnt a large amount of work. Its the annoyance of the work and the leaky abstraction of "multiple returns" that _look_ (and sort of act) like its a tuple. In addition to channels, it also comes up if you want to store an array of return values.


even more annoyingly, you can define a function that returns it but not a function that accepts it, so you often have to jump through a lot of hoops to combine higher order functions with multiple return values.


Not sure what you mean, functions can take multiple return values as arguments just fine: https://play.golang.org/p/l7s7i-QyRnM


_sort of_. Again, the leaky abstraction comes through when you try to add additional params in addition to the multiple return:

https://play.golang.org/p/lwMBkvnaVm0


Personally I find the Rust syntax is a nice middle ground. If the function returns a Result, it can throw an error. And unless you provide a From<> implementation you do need to handle the specific error that gets thrown. But if you want things to bubble up cleanly you provide From<> (or use a crate that provides it for you) and it all works wonderfully.


> python server application, which involved a lot, "oh shit, that throws" whackamole.

This matches my experiences with Python, and docs, even official Python docs, are somewhat lacking when they describe what might get thrown.


> "oh shit, that throws" whackamole

You don't have that problem in languages that support checked exceptions.


I've never seen a language that supported checked exceptions and actually used them consistently for everything.

But then that would be kind of awful, because IMO the checked exceptions cases for "I know this will never actually throw, leave me alone" and "This is just a quick script, go ahead and blow up the whole program if this fails" are poor, forcing you to write try catch blocks all over the place.

IMO, Rust has a nice approach for being stricter - unwrap. If it's a quick script, throw unwrap everywhere with reckless abandon. For real programs, grep for unwrap and make sure for every one, you really know for sure that it will never fail or that blowing up the whole program if it does is the right move. To convert from one to the other, grep unwrap and actually think about what you want to happen if that line is Err.


You may still have that problem if the language also supports unchecked exceptions.


Agreed, mostly true.

(It's still not as explicit about where your function might exit, but you won't get surprise exceptions, which is way nicer.)


You do and you don't. If you're using a Java library written by someone who doesn't believe in checked exceptions in Java, you might be lulled into thinking everything will be fine.


In Rust, your code would look like this:

  fn foo() -> Result<i32> {
      let result = bar()?;
      let result2 = baz()?;
      let result3 = bazz()?;
  }
(This wouldn't compile because you don't actually return anything in the success case, but I just kept the code the same. Additionally, I am assuming you are using some sort of crate that helps with error type mapping, because that's realistic, otherwise, it would be Result<i32, Error> where Error would be some sort of type mapping to SomeException's type.)


> How do people using Go and other languages that don't have exceptions solve this reduced readability problem?

We don't, we just make memes about it and move on.


HA! That error handling was one of the more tedious parts of golang to me. Though Rust has it's own quirks, the error handling was one of the more compelling reasons for me to switch my side projects to use Rust instead of Go. That being said, Go is much more fully featured than Rust and a fantastic language.


>The problem with error code return is tediousness

Tediousness is only relevant at the time of writing the code, which is the least important part of programming. You read code 100x times as often as you write it, writeability is simply the wrong aspect to optimize for.

>How do people using Go and other languages that don't have exceptions solve this reduced readability problem?

Readability isn't reduced, it's enhanced - the control flow is explicit, there's no magic in the form of implicit returns.


> Tediousness is only relevant at the time of writing the code, which is the least important part of programming. You read code 100x times as often as you write it, writeability is simply the wrong aspect to optimize for.

I'm kind of tired of this argument, as if the writability of a language doesn't matter at all. Over a long enough time frame, with a large enough team, etc, this is absolutely true, but when you're writing features _now_ that need to be released ASAP so the company can make money, the speed at which you write code is one of the most important factors to your productivity. Language ergonomics matter tremendously for this, affecting the time it takes to write, the chance for mistakes, and the amount of frustration you feel.


Error checking should also really be enforced by the language. It's too easy to forget otherwise.


Thank you for saying this. It seems like not many PL enthusiasts get this.


Checked exceptions are evil because they can cause transitive API breakage in cases a function or method doesn’t catch because it can’t or shouldn’t handle the condition.


Since this thread is also about Rust, and since I mention in another comment about how Rust libraries define their own error types, I should mention that Rust can avoid this problem by the combination of 1) adding a new way that a function can error only requires adding a new case to the library's error enum, leaving the function's type signature unchanged, 2) adding new cases to the error enum can be kept from being a breaking change via the use of the non_exhaustive attribute, which requires anyone manually handling the error to add a default case even if they handle all the other existing cases. Of course, if library consumers are just throwing a panic in that mandatory default case then you might be trading a compiler error for a runtime error, which can be controversial. The language design challenge of this decade is finding compromises between allowing libraries to change and evolve while mitigating the disruptiveness to downstream consumers.


Sorry that doesn't make any sense to me. If a method can't or shouldn't hand a condition it then it needs to declare itself as throwing that exception (or a wrapper around it).


They said transitive. To me, they meant if you have 3 layers of deps, A -> B -> C and C now throws something, B doesn’t catch it or mark as throws. So now you have a failure. Obvs, you’d fix this immediately, but it’s still work that you have to do when you could have been doing something productive.


Here's Hejlsberg on why C# doesn't have checked exceptions: https://www.artima.com/intv/handcuffs.html.


Hejlsberg is an awesome compiler maker. His Turbo Pascal compiler was incredible -- in approx 30KB he wrote a compiler, full screen editor and library. He's not that good as a language designer. C# is basically a clone of Java with broken exception handling.

Here's the problem with C# exceptions: https://web.archive.org/web/20041201011903/http://www.geocit...


The Either monad is a very nice solution for this in my opinion, in Haskell it's just

  do bar
     baz
     bazz
with it immediately returning after the first error value it encounters or returning the actual result (this is also how Rust solves it, though they introduce specific syntactic sugar for it rather than an universal monadic sugar).


Rust has an operator for "re-throwing" error: question mark.

  fn foo() -> Result<int, SomeError> {
     let result = bar()?;
     let result2 = baz()?;
     let result3 = bazz()?;
     Ok(result + result2 + result3)
  }
It's still 100% more explicit about return types and errors, but has the same lightweight feel as in JS/Java/etc.


In your example if bar() returned an error your program still needlessly calls baz() correct? This is why exceptions are better.


Nope, the question mark operator is syntax sugar for the "if error, return it" dance.

(Cf. https://doc.rust-lang.org/edition-guide/rust-2018/error-hand...)


That code desugars to the following

  fn foo() -> Result<int, SomeError> {
     let result = match bar() {
         Ok(val) => val,
         Err(err) => return Err(err.into()),
     };
     let result2 = match baz()  {
         Ok(val) => val,
         Err(err) => return Err(err.into()),
     };
     let result3 = match bazz() {
         Ok(val) => val,
         Err(err) => return Err(err.into()),
     };
     Ok(result + result2 + result3)
  }


Nope, it returns an error if bar returns an error - immediately. It's essentially shorthand for `if x.is_none() return X;`


The problem with exceptions is they make it way too easy to not handle and error. At least with the first example you had to at least think about the error as you typed return err.


That and `catch em all` attitude being promoted


You're not supposed to catch exceptions. They are not for handling logic. They're there to make fatal errors across threads and modules be safe and robust.


To add on to what other commenters have pointed out with `?`, there's also the interesting [fehler crate](https://github.com/withoutboats/fehler), which lets you write code that _looks_ like it uses exceptions.

It wouldn't make the examples that already use `?` any clearer, but it does reduce noise around the happy path.


I believe that withoutboats has deprecated that, and now recommends anyhow (for applications) or thiserror (for libraries).

I’ve tried them, and they really do take most of the toil out of defining errors.


Pretty sure you're talking about Failure, not Fehler. Failure is (like Anyhow and Thiserror) for defining error types, Fehler is an alternate syntax for the functions that return those errors.


fehler isn't deprecated, but _I_ did deprecate the similarly named https://github.com/rust-lang-nursery/failure/ with withoutboat's blessing.

failure proceeded fehler by several years.


> How do people using Go and other languages that don't have exceptions solve this reduced readability problem?

Having a single clear return path with, e.g., monadic returns and pattern matching is more readable than exceptions (particularly, the signatures are more readable than with checked exceptions effectively splitting the type system into two separate components with distinct syntaxes).

OTOH Go (which has a very close equivalent of exceptions but idiomatically prefers using it's annoying multivalue return pattern in most public APIs) is pretty much the worst of all worlds.


There are roughly three kinds of exceptions.

The first is the set of exceptions that could potentially be caused by any basic instruction that are really more of a "the programmer screwed the code up, and our static type system isn't powerful enough to prove it can't happen." Think Java's RuntimeException, the basic processor signals in C/C++ (SIGSEGV, SIGFPE, SIGILL, SIGBUS), or Rust panics. For these exceptions, the standard handling behavior is to catch it at a very high level, log as much details as you can about what caused the exception, fail the task (e.g., return HTTP 500 error), and throw away the detritus. These exceptions should be as invisible to the user as possible, and ideally you want to spend a lot of time at the point the exception is thrown collecting as much information as possible for logging purposes (such as stack traces).

The second class is the exact opposite: you're doing a simple operation that is expected to fail. Such as opening a file (it might not exist) or querying a hashtable (the key might not exist). Errors here are almost always going to be handled immediately by the caller, and so collecting a lot of details about the error is not usually what you want to do. Using a simple error code for return is precisely what you want to do in this situation.

The final class of exceptions is perhaps the most common: you have failures that need to be communicated from point A in the system to point B somewhere distant. My prototypical example is a (recursive-descent) parser: if the lexer sees a token it doesn't understand, you want the entire lexer and parser call stack to propagate the error to the caller of the parse function. Sometimes with these errors, you want or need to attach more contextual information as they propagate: in a SAX-like parser, you might want to explain the path to the token that failed.

For the last error class, error-code handling is clearly too cumbersome for most use cases. The easy propagation of C++/Java-style exception helps, but unifying the underlying implementation with the first implementation may be unnecessarily penalizing speed. And the silent propagation turns out to be a disaster for both user maintainability and compiler optimization. That such an exception can be thrown needs to be a fundamental part of the type of the function. Java introduced checked exceptions to express this type correctly, but the way exceptions integrate with the rest of the type system has turned out to be less than satisfactory.

On top of these exception classes, there are two more important considerations. The first is the general need to recognize chaining of exceptions: you may need to explain the cause of a high-level exception in terms of a specific low-level exception. The second is that you may want to convert between the different classes--turning a file-not-found exception into a hard program crash, for example. (This is especially useful when writing exploratory code!)

In my opinion, none of the languages I'm familiar with have solved all of the issues with exceptions. Exceptions are hard to design right, and since they're so fundamental to how you express code, they're a feature that is baked so early in the process and is difficult or impossible to change as you discover problems with it. I do think that Rust's Result-versus-panic, with the ? syntax for Result propagation (as noted by several sibling comments) is a good starting point, but my feeling is that more work needs to be done on easing the burden of specifying exception types when you're plumbing together many stages and libraries.


Change your recursive-descent parser so it pulls source tokens from a socket as opposed to RAM. All networking IO is expected to fail. You probably don’t want to handle socket errors in the parser because there’s nothing you can do about them there. You want to handle them very far away, telling user you’re unable to parse stuff due to a network error.

Some libraries expose parallel APIs for these two cases. For example, hash maps from C# standard library exposes both operator[] which throws exceptions when trying to get a value that does not exist, and TryLookup which returns a boolean.

Overall, I think exceptions are solid in C#. There’s only one class of exceptions there, they can include lower-level ones, can aggregate multiple of them, for native interop they have 32-bit integer codes and runtime support to converting codes to/from exceptions, have runtime support to preserve contexts to re-throw later (e.g. on another thread). Uncaught exceptions become hard crashes.


> Overall, I think exceptions are solid in C#. ... Uncaught exceptions become hard crashes.

Do you see the problem, these two sentences are juxtaposed? When exceptions are not part of the contract you would have to read the source code of every method you call (and methods they call) to know what exceptions to catch.

Even then, a method you call can be modified later to throw a new exception and your code will crash. The only solution is to use the root Exception class which everyone agrees is a bad idea.


> When exceptions are not part of the contract you would have to read the source code of every method you call (and methods they call) to know what exceptions to catch.

Even that won’t help you to find that out when the API takes callbacks or interfaces.

> is to use the root Exception class which everyone agrees is a bad idea

It allows to use lambdas (called delegates but that’s technicalities) and interfaces in APIs. Unlike C++ which allows to throw anything (probably because the standard library with std::exception was too late to the party), in C# the language guarantees that will cover 100% of the exceptions.

The idea is decades old and probably predates exceptions. For example, some pieces of Linux kernel APIs say in the documentation something like “return negative error code if failed”. What you see in almost 100% of use cases of error handling is if( result < 0 ) condition, not a switch-case testing for individual codes or their ranges.

Catching individual exception types is usually a bad idea. Software is incredibly complicated these days; we use tons of APIs and libraries implemented by unrelated people from different continents.

If the language doesn’t make them part of the verifiable contract, you’ll get runtime errors once the implementation changes and a new version starts throwing another exception type.

If the language makes them part of the contract, you’ll be wasting a lot of time making your code compile again after a dependency is updated. This creates an incentive to never update them.


>Even then, a method you call can be modified later to throw a new exception and your code will crash. The only solution is to use the root Exception class which everyone agrees is a bad idea.

This view was (is?) very widespread in the Python community, which led me astray as a beginner. Trying to find each individual exception that a function can throw when you really only care if it errored or not is a fool's errand. These days I just catch the base Exception, and only catch specific subclasses of it if there's an actual need to.


>How do people using Go and other languages that don't have exceptions solve this reduced readability problem?

Rust has ? operator which converts "let x = foo()?" into "if foo returns Ok(value) assign value to x, otherwise make the function in which it is used return given error". Go doesn't care about readability.


Go cares very much about readability. It’s a very subjective topic...because someone doesn’t agree with your opinions on a subjective issue doesn’t mean they don’t care.


Sure, people say they want to have to check for _every_ possible error, but then Java gets endless heat for having checked exceptions.


Yes, this schizophrenic attitude drives me bonkers. Immature JS kids at my company love to make noises about Java’s verbosity yet spend hours debugging and troubleshooting most mundane errors because seemingly nothing is ever enforced by the JS interpreter.

My theory is that nowadays Java is unpopular mostly because it is their parents’ programming language. That’s reason enough for these twenty somethings to despise it without even knowing the first thing about it.


Hold on, though. There is plenty to dislike about Java. Everything can be null, interfaces have to be implemented at definition, broken array type variance, no const/mutable at the language level, no value types, etc...


No question there is a lot to dislike about java but there are also things it got mostly right or better than competing languages of the day. Checked exceptions is in my opinion one of those.


I agree with that. I was heavily involved on the other thread someone linked to about Java's checked exceptions. Until that thread, I thought I was the only one who didn't accept that they were a mistake.


I resonated with the post as I'm in a similar position but I'm really sad that typescript chose just to be a thin layer on top of javascript.

The sentiment mostly comes from having the JS ecosystem mostly being untyped and having to interact with it.

That being said I tried io-ts but found it undocumented, missing examples and hard to write. For future libraries/projects I'm looking to try again ReasonML, tried in the past but had to write too many bindings.


Author here. I used io-ts when the documentation is still on its root README.md. Apparently it has been moved to https://github.com/gcanti/io-ts/blob/master/index.md


I've been rolling io-ts into an app I inherited and have been porting to TypeScript. If you know a little Haskell/Scala type FP, the docs are plenty. I think the lib could definitely have a shallower learning curve for folks coming from Go though (what my back end devs write mostly).


IMO the `Either` construct should be avoided in JS because inevitably you'll either need to wrap everything in `try ... catch` anyway or you'll be pulling your hair out trying to figure out how to get them to work in an async/event context, in which case you'll end up re-inventing the Promise api (perhaps wrapped in an extra thunk to make it more fp-friendly or whatever).

A more practical approach is to work along the grain of JS while getting inspiration from fp. A common snippet would be:

   function maybeUrl(str) {
     try {
       return new URL(str);
     } catch (err) {
       return null;
     }
   }
This is much more straight-forward and interoperable. Dealing with something like Left<InvalidUrlError> where you've created an entire error subclass and wrapping function for an unrecoverable failure that will be discarded is way overkill.

The unreliablility of thrown values in JS is a valid concern, but instead of trying to wrap all my code in a container construct, I simply wrap/convert the untrusted errors themselves if I need to use properties on an error reliably.


Totally agree.

In cases where the error type matters, I like to return rather than throw the error.

    type Success = string
    type Result = Success | BadInputError | NotFoundError | IncalculableError

    const foo = (input: I): Result => {
      //...
   }


Don't write code like this... trying to fake sum types in an object oriented language ends up being a horrible, hard to maintain mess, similar to those examples on the original post.

Typescript's "discriminated unions" [1] make for incredibly inelegant looking code for anything but the most basic sum types, the ergonomics are nothing like the experience of using Haskell or OCaml.

I love sum types but in an OOP the equivalent implementation is the visitor pattern (if the problem calls for it). I was once starry eyed about "making invalid states irrepresentable". I even managed to introduce a code generator to bolt sum types into a production Dart app. My colleagues loved it (not) :-)

Thing is, programs have this annoying tendency of being incorrect no matter what technique you use :-), there's no silver bullet! If a pattern is natural for a given language, it is better to use that pattern than to force it to be something it isn't.

1: https://www.typescriptlang.org/docs/handbook/unions-and-inte...


The joke is, OO language have sum types, albeit incredibly broken ones, because any set of classes that inherits from the same base has the base as a sum type.

You can surface this by implementing the visitor pattern.


The first example code is rather off-putting, because it has multiple errors. The code would not throw an exception, because it produces compiler errors so you’ll never run it. (OK, so it still generates JavaScript that you could run, but I’m assuming you’re using TypeScript properly and won’t do so.)

• `return number + 1;` should be `return someNumber + 1;`. If you ignore errors and run the JavaScript, this means that it’ll throw a ReferenceError on this line.

• `bar(someNumber) + bar;` is adding a number (well, it should be a number, but you’ve omitted the return types, so the previous error will mean it infers `any` instead of `number`) to a function. This is a type error.

• `baz()` is called with zero arguments instead of the one it requires.

> When baz is called it will throw an uncaught exception. By reading the code you know that foo throws an Error. Debugging this snippet is a piece of cake, right?

As mentioned, this code doesn’t compile, but if you ignore that and run it anyway, then the uncaught exception is a ReferenceError, not the error you see on a throw line. I… don’t think this is what was intended. This also demonstrates what I consider a bit of a weakness of the exception system in such languages: various programming errors that a compiler should catch come through as runtime errors instead.

(I’d generally prefer to use exceptions to indicate exclusively programmer errors, and basically never use a try/catch block, but too much is designed around treating exceptions as normal parts of control flow to be able to do this in most cases. I like the Result/panic split in Rust, where panic always means programmer error, and Result is for probably-exceptional cases, but where they’re still generally expected and part of the design rather than programmer error.)

If you fixed only the first error, you’d get a value like "NaNfunction bar(someNumber) {\n return foo(someNumber) + 1;\n}" out of the baz() call. Fun times.


Author here. Caught red-handed not running it on playground. Thanks for the correction.

> I’d generally prefer to use exceptions to indicate exclusively programmer errors

The idea that panic/exception means programmer errors worth to be noted. It think they should be intended for errors that is intended to be unrecoverable. Still, catching errors on runtime is not fun


Author has now fixed the example: https://news.ycombinator.com/item?id=24454455


Reminds me of a pattern I've wished would take off in TS, but hasn't (yet):

    const result = await fetchData().catch(e => e)
    if (_.isError(result)) {
      return "some failure"
    }
    return (await result.json()).foo
TS is smart enough to know whether result will be of type Error or the Response type.

This pattern should replace most Either cases when the left is an error state of some kind. Also should replace exceptions and promise rejections.

You can also analogously replace a Maybe type with nullability, simply `type MaybeFoo = Foo | null`.


I agree with Maybe not being that useful in TS but the point of an Either/Result type is that the caller is gonna handle it each branch differently. In your example the caller has no way to tell the difference between an error and the succesful return value.


Perhaps I was unclear, the implicit typing is so:

    const result: FetchResult | Error = ...
    if (_.isError(result)) {
      // Here TS knows that result is of type Error
      return
    }
    // Here TS knows that result is of type FetchResult

On my phone, apologies for shorthand

In any case, with this pattern TS can easily tell the difference between success and error (or left and right or whatever) as long as the error / left case is a class of a different type than the success / right case.

Tldr, where relevant, classes are cleaner than tagged disjoint unions in TS. Errors are one situation where this is relevant.


I’ve seen and used the Maybe version of this a few times, I really like this Typescript feature. Using it for errors is clever :)


Tangent: This article made me think deeper about programming languages in general and I found it fascinating. So I went to subscribe to his blog in my feed reader, but alas, no RSS feed. :-(


Author here, thanks for the input. Forgot to put it in my backlog :pepesad:


Rolled out!


Thanks! Subscribed.


You can write bad code in any language, Rust included. This article is cherry-picking.


I think it’s best to stay language agnostic especially in 2020. I try to think data-in data-out and use the best tools available for clean execution.


"Language agnosticism" is an ideal I have never been able to attain. Sure, I can hack together a solution on the first week of learning a language. But, am I able to write elegant, idiomatic, efficient code during the first few months? No way.

A language is not just the basic control flow operations, but includes package management, performance gotchas, correctness gotchas, standard library, library ecosystem, data representation, etc. Then there are details and edge cases that you just don't hit without months or years of hard work with a language.

My experience tells me to take choose a reasonably robust language, then stick to that language until you have an overwhelmingly compelling reason to switch.


> and use the best tools available

How do you know which tools are the best?


0. Define what problem you are trying to solve, make sure you understand the domain.

1. Come up with a set of options by doing research, talking to people who have solved similar problems before, etc.

2. Rank them by some criteria (e.g. availability of libraries, documentation, raw performance, hireability, support, quality of tooling, etc).

3. Do some prototyping in your problem domain using the top 2-3 (more if you aren't confident in your ranking) and pick whichever you like best.

4. Always be willing to change your mind down the road. Avoid the sunk cost fallacy and try not to lock yourself in too much until you're confident in your choice.


Understand a lot of programming languages, become proficient in a few and use your understanding when trying to come up with solutions for your problems. At least play around in other technical stacks then what you normally use, so you get a broader understanding.


Didn't mean to cherry-pick

I believe that programming language has the power to push its user to write in a certain way, not just a mere tool to convert idea to machine code.

What I learnt by writing Rust turns out to be useful when applied in other language.


I disagree. Look at what that guy did with C macros. He was Fortran lover and wrote entire programs in C using Fortran syntax by re-purposing macros for that goal. You always write in same style, regardless of programming language.


That does not negate my argument though

My argument was:

> I believe that programming language has the power to push its user to write in a certain way

Not:

> I believe that programming language has the power to enforce its user to write in a certain way

In contrast, your argument here is more debunkable:

> You always write in same style, regardless of programming language.

You cannot ALWAYS write in the same style with disregard to your programming language choice. The black swan: Try writing GLSL and Haskell in the same style


You don't actually need the tag on Either. This is how I defined my Result type. The shape is enough to discriminate them.

    type Result<S, E> = Success<S> | ResultError<E>;
    type Success<S> = { value: S };
    type ResultError<E> = { error: E };


Author here. Did this too. I found some quirks (rather irrelevant) while compiling using TypeScript 3.6-ish.

``` const { value, error } = result; if (error) return doSomething(); if (value) return doSomethingElse(); ```

^ You can't do this because value or error might be not an attribute of result

Nowadays I will just `npm install fp-ts` and use them.


You're right that you can't destructure until you've established which side of the union you got, does the tag help with that?

I like a Result type over an Either because it feels semantically more meaningful. I work with people with a wide range of background and a Result type is self explanatory, they can just read the code by themselves without knowing and understanding the convention of which side of the branch the error goes, etc...


I agree with you on the Result type being semantically more meaningful and I agree there are a lot of conventions to be remembered coming from functional programming that gets meta.

Thanks for the insight


I'm very unconvinced considering the section on avoiding exceptions doesn't discuss async code at all. I mean, a returned Promise from an async function is very similar to the author's "Result" object where you can even pass a success handler and a rejected handler right in the single "then" method. That said, I greatly prefer instead to await on an async function and handle any rejections in a catch block.


Exceptions aren't typed though so you either have to decode it in some way or treat every exception the same.


Please please please don't perpetuate the use of either or maybe types :(

They make software very hard to maintain and end up breaking anyone who depends on a library that uses them.

Rich Hickey (author of Clojure) has a great discussion of Either and Maybe here: https://www.youtube.com/watch?v=YR5WdGrpoug that dives into their unsoundness.


Rick’s argument seems to be that we should have better tools in the language type system. If your language doesn’t give you those tools, these are still very useful types. In addition, the library maintenance issues he raises seem trivially solvable with languages that allow function polymorphism or have refactoring tools to me.


The author of a dynamically typed language thinks strongly typed constructs are bad? You don't say...


With JS and Typescript, I find myself going back to logic error handling in the catch block, because there is only one Error type. You can't have multiple types of exceptions. So you have to set the error.type attribute and then do logic.


It is worth noting that you can throw anything you want. It doesnt' have to be new Error(). There are definitely some drawbacks to this, such as if the errors thrown by your dependencies are of type Error, but the upside is that as long as you are consistent, you get a much richer set of types available.

IMO this also works primarily because errors are very rarely thrown by native APIs, which instead encode failure into the return type. For this reason, you can typically have a reasonable level of control over what code can reasonably be expected to throw.


Why do you need Either when you have union? All the left types subclass Error so you can easily separate that from the successful value as well. Seems rather a dogmatic choice.


Does anyone else hate foo(), bar() and baz() as example names. They are meaningless - except communicating that its a programmer selected name. Even func1(), func2() would be better.


f(), g() and h() are decent alternatives.

Of course, the real solution is to come up with an reasonable example scenario with meaningful names.


a, b, c seems more intuitive


Yep.


I dont think it matters


I'm ok with it but I think it does matter a bit.

here's a small example:

"Did baz call bar or did bar call baz ?" "Did func2 call func3 or did 3 call 2?"

Again not a big deal but if I'm trying to show a snippet of code then I would try to make that snippet as simple to follow to get my point across. I think it's a sensible rule.

In this case, could it have helped if instead of ,hypothetically, keeping this information in your head "foo called bar which baz" you could rely on meaningful names to help the reader ? Yes. Yes I think it would be beneficial (matters a bit).

Whether that's specifically "func2"..3 or very verbose names "funThrowsExceptionOn0()", I think it would help.

You can argue if that was necessary here for this example but I think it'd be hard to argue against meaningful names.


[flagged]


Would you please stop creating accounts to break HN's guidelines with?


The gatekeeping isn’t necessary. Some people pick up programming because they have an idea they want to implement, rather than because they want a rigorous formal education in computer science.


[flagged]


Author here. Input and further discussion would be appreciated.


Well exceptions are generally a sign that code smells. Instead of throwing an exception, just return the default value.


>code smells

This is one of the worst "ideas" ever in programming. It allows any programmer to shit on any other programmer's work in practically any arbitrary way they feel like.

Your idea of returning default is considered by others here to be a "code smell" - if you want to call it that.

Looks like using that term just doesn't give your idea any benefit the way you thought it would.


Honestly that sounds substantially worse in pretty much every way in nearly every situation. If an assumption of your program has broken, you almost always want to do something about it rather than simply silently producing garbage. Even in a less expressive language like C you'll still often use e.g. a negative return code to indicate failure.


How do I know if it errored or not? The default value is ambiguous. It either means that it's the correct result or that the function errored. Sure, the program might not crash right then and it will compile, but it's guaranteed to lead to confusion for both users and programmers who have to debug it.


This is worse in every possible way. If the default value was part of the domain of the function it would be impossible to distinguish an error from a legit answer without breaking encapsulation. If it wasn't, then you're handling errors like in C.. ugh


> 1 / 0 just returns 0, #yolo

No thanks.


Returns infinity like it should


Stopped reading after the exception / result stuff. You can have both, both are useful, learn yourself some Swift maybe.


I think monads are especially useful when the whole eco-system is build around it.

Using promises with callback APIs gets ugly quickly.

Using results with exception APIs also isnt much fun.

Rust shines here because it's the default way.


I can see the author's point, that a throw behaves awfully like a GOTO. It can add complexity and unpredictability to the code path, especially dealing with external modules.

On the other hand, there are situations in which throw/catch provides a simpler, even elegant way to handle errors.

So I'd agree with you that both are useful.

The author makes a good point though, and I will consider it next time whether returning errors may be more suitable, to be clear and explicit/verbose, without requiring the GOTO-like control flow.


Another place throws are inconvenient is chaining/streams.

Pseudocode:

    var goodStuff = listOfStuff.map(x => transform(x))
        .filter(t => typeof(t.result) != Error)
        .map(y => handleGoodData(y))
If `transform()` threw exceptions, this whole thing aborts. But if it can return an `Error` type, we can continue processing the good ones and log the bad ones (here I ignore them for simplicity).




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

Search: