From Rust to TypeScript
valand.dev
valand.dev
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...
You caught me red-handed for not testing these codes in playground.
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.
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
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.
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.
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.
Don't forget JavaScript (and therefore typescript) will let you concatenate a lot of random types together without error.
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).
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
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?
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...
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)
If they were to be part of the standard, it would be another story.
{
"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)`Object.values(SomeEnum)` is pretty handy, something you can't do with string union types.
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
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.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?
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).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
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.
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.
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.(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/
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.)
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).
This matches my experiences with Python, and docs, even official Python docs, are somewhat lacking when they describe what might get thrown.
You don't have that problem in languages that support checked exceptions.
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.
(It's still not as explicit about where your function might exit, but you won't get surprise exceptions, which is way nicer.)
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.)We don't, we just make memes about it and move on.
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.
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.
Here's the problem with C# exceptions: https://web.archive.org/web/20041201011903/http://www.geocit...
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). 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.(Cf. https://doc.rust-lang.org/edition-guide/rust-2018/error-hand...)
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)
}It wouldn't make the examples that already use `?` any clearer, but it does reduce noise around the happy path.
I’ve tried them, and they really do take most of the toil out of defining errors.
failure proceeded fehler by several years.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 => {
//...
}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...
You can surface this by implementing the visitor pattern.
• `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.
> 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
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`.
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 shorthandIn 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.
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.
How do you know which tools are the best?
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.
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.
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
type Result<S, E> = Success<S> | ResultError<E>;
type Success<S> = { value: S };
type ResultError<E> = { error: E };``` 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.
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...
Thanks for the insight
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.
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.
Of course, the real solution is to come up with an reasonable example scenario with meaningful names.
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.
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.
No thanks.
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.
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.
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).