Hacker News new | past | comments | ask | show | jobs | submit login
The problem with new URL(), and how URL.parse() fixes that (kilianvalkhof.com)
130 points by fagnerbrack 8 months ago | hide | past | favorite | 171 comments



Why would you want to have a constructor of type T that returns you something that is not even an object of that type T? If you need the "return null on error" behaviour — so that you can never check that it's null (or forget to check) and let your execution stop with "TypeError: Cannot read properties of null" way down the line instead of right here, where you can fix it immediately before even shipping the code — then just write a tryParse() wrapper for that!

Now, to be fair, having a public constructor for URL with just a string as the argument, with the parsing semantics is arguably an anti-pattern, and a static factory function would serve much better indeed. A proper public URL constructor should instead take (scheme, authority, path, query, fragment) tuple and validate its constituents. The private URL constructor could be the one without validation, and that's what parse() and the public constructor would call internally.


It doesn't help that there's no way to specify in the contract of the function that it can throw. Blowing up your entire program because it can't parse a URL is something you should be made explicitly aware of.


Checked exceptions have been tried in Java, and were universally hated, mostly for poor ergonomics.

A very similar thing, the Result type in Rust, does pretty well. (Much like Maybe / Either do in Haskell.)


Why? I never understood the objection to checked exceptions. It seems to me they would give me exactly what I want! (Granted, I'm a Haskell programmer, not a Java programmer.)


On top of all the other problems pointed out, java's checked exceptions don't even do a good job of indicating possible failure conditions. The standard library set the tone on this with exception like IOException, which has an enormous amount of subclasses represting different IOExceptions, and is thrown by anything related to IO. So I still need to rely on documenation or code inspection to understand what exceptions might actually be thrown and what they mean, if I want to recover from errors.

The classic case of this is that the compiler cannot tell that

    new StringReader("Example").read()
doesn't throw an SSLException.


But, you're conflating a language feature "Java's checked exceptions" with bad API design, which is obviously possible with any combination of any language's features. I agree that Java's standard library has a ton of bad APIs--not just the checked exception hierarchies, but that does not necessarily mean that checked exceptions are a bad idea.

Just to put an extremely fine point on it, one thing I always complain about is the JDBC ResultSet API. If I have a SQL database table that has a nullable integer column, and I query that column for a row that happens to have that column set to `NULL`, then the ResultSet API will give me the int value `0` for that column and I have to just know that I need to call `wasNull()` (or whatever it's called) to ask the ResultSet if the last thing it returned was really supposed to be null or not. Yet, the ResultSet API does not make me think that Java shouldn't have `int`, or class methods, or any way to query databases--it only makes me think that the JDBC API is really, really, poor.


The question was, why checked exceptions saw no love. A big part of the answer is that the stdlib did a very poor job showcasing them.

No wonder, it was an attempt to graft an FP-esque feature onto a deeply imperative OOP language, with a development team aligned with OOP concepts. I suspect the stdlib team lacked either the understanding or the means to make the use of e.g. IOException nicer. They didn't even have type parameters at their disposal yet. It was botched as a result, and deprecated.

It's an interesting mental exercise to try and design a better implementation of the idea within the constraints of Java. (Implementing the monadic approach is likely the easiest and cleanest way.)


> The question was, why checked exceptions saw no love. A big part of the answer is that the stdlib did a very poor job showcasing them.

Fair enough. I can see that interpretation. I was more focused on their first sentence, which said: "On top of all the other problems pointed out, java's checked exceptions don't even do a good job of indicating possible failure conditions." Then it felt like they were backing that claim by citing a specific example that's just a poor use of the feature.

> No wonder, it was an attempt to graft an FP-esque feature onto a deeply imperative OOP language, with a development team aligned with OOP concepts.

That's an interesting take that I hadn't really heard before or considered myself. I had assumed that it came from some frustration around C++ exceptions and not knowing what was supposed to be handled vs not.

But, in any case, I do agree that the feature ended up rejected by the larger programming community largely because of Java's specific implementation of it.

Though, I have to say that I think it might have ended up rejected even if Java did a perfect job of it in the standard library. The truth is that most programmers don't seem to understand the intended design of the feature and feel like there's a binary choice between using checked exceptions or unchecked exceptions. The Oracle documentation does a very good job, IMO, of explaining the feature, how it's intended to be used, and how do decide whether to use an unchecked exception or a checked one. But, I don't think most people actually read manuals/books to learn programming languages anymore.


Especially in more modern Java, a huge problem of checked exceptions is that they are not parametrizable. So you can't (easily) call map(list, foo) if foo throws an unchecked exception, since map() doesn't throw, and there is no way to make map() throw conditionally.


Swift has the concept of conditional throwing for closures/callbacks. The keyword is `rethrows` if anyone wants to do a search and read some docs.

Now, Swift's version of throwing is what I would call "semi-checked", because it does force callers to handle errors from functions that have `throws` in the signature, but it doesn't specify the specific type of error that may be thrown.

But, in any case, that kind of proves that there are other ways to do checked errors/exceptions that are not exactly as Java has done it or the monad-style return value approach that a lot of other languages have done.

I really think the entire static typing programming community threw the baby out with the bath water when "we" collectively decided that checked exceptions were a pariah language feature.

Also, I'm pretty sure that Java's checked exceptions are parameterizable to a large degree. I believe you can write something like (forgive syntax errors),

    interface Foo<E extends Exception> {
        void doSomething() throws E
    }
That doesn't fix the issue that you're specifically addressing, which is that you can't write "this might or might not throw, depending on what you pass to it", but it's more than I think many people realize they can do.

And, one more point: I don't think it's actually a problem that the Stream methods don't allow for throwing. A "map" from one type to another is not really supposed to be fallible--it's not a "mapping" if that's the case. That's a case where an imperative loop is more semantically appropriate, IMO.


Thanks for the swift pointer, I’m not too familiar with that language.

As for the more formalized version, effect types come into the picture, that let a function be parametric in how it handles its functional parameters. E.g. a `map` could be throwing if given a throwing f, but non-throwing otherwise. But these can even handle more complicated effects as well.


This is the vast majority of the problem. As a consequence, APIs with callbacks or wrapping generic things of any kind are forced to choose between "throws Exception" and a runtime exception, losing all detail and possibly choosing the opposite of what is actually needed. And there are TONS of these, and they're heavily used.

It's a badly crippled implementation that has poisoned the well, the concept is just fine. People like Result<T,E> well enough, and that's exactly as expressive as checked exceptions in languages that implement them correctly.


while `Result<T,E>` is very similar to an exception there are a couple of limitations:

1. results must be handled explicitly, exceptions are forwarded automatically. This means I must deal with each result locally, even with a "forward up" to the caller. This means much complex and less readable code

2. exceptions have a stacktrace, handled automatically for you. This means more precise location of where the error occurred. For the same reasons exceptions are heavier - as they keep more data AND is updated for each frame (when handled, and depends on the implementations)

3. the syntax to deal with exceptions is ugly as hell. I haven't found anything better than the python syntax, and still is not something you like to see.


> 1. results must be handled explicitly, exceptions are forwarded automatically. This means I must deal with each result locally, even with a "forward up" to the caller. This means much complex and less readable code

Rust address this very concisely.

In Rust, "forward up if error" is one character, '?', and you can't forget to use it because it also unwraps the non-error value.

In Go on the other hand, "foward up if error" is a notoriously repetitive multi-line sequence. Some people like it because it forces every step of error forwarding to be explicit and verbose. Some people don't.

In Go I've worked with, there's a lot of these. People aren't avoiding error handling to save keystrokes. But occasionally I've found a mistake in Go error-forwarding that went unnoticed for years and would have been detected by Rust-style statically-typed Result, or any kind of exceptions, so I'm not convinced the Go-style explicitness really helps avoid error-handling bugs.

> 2. exceptions have a stacktrace, handled automatically for you. This means more precise location of where the error occurred. For the same reasons exceptions are heavier - as they keep more data AND is updated for each frame (when handled, and depends on the implementations)

That's true in practice, but it's a design choice, not a strictly required difference between exceptions and Result<T,E>.

In principle, the heaviness of a stacktrace can be eliminated in many cases, if the run-time (with compiler help) keeps track of whether an exception's catch handler is never going to use the stacktrace (transitively if the handler will rethrow).

Lazy, on-demand stacktraces are also possible, saved one frame at a time on error-return paths, and sharing prefixes when those have been generated already. These have the same visible behaviour as normal stacktraces, but are much more efficient when many exceptions are thrown and caught in such a way that the run-time can't prove when to omit them in advance.

In principle, the same things can be applied to Result<T,E> types: Take a stacktrace where the Result is constructed, and use the above mechanisms to keep it efficient when the stacktrace isn't used. In Rust, there are library error types that save a stacktrace, if you turn the feature on, but I don't think any of them automate their efficient removal when unneeded, so turning the feature on slows programs unnecessarily.


I agree that, in principle, Rust's mechanism could be the best of all worlds. Unfortunately, the fact that ? doesn't add any kind of context makes it a non-starter from my point of view. There's nothing worse than a log saying "Failed to perform $complex_multi_step_operation: connection failed". Even Go's ugly verbose error handling pattern is better than building up no context at all.

I don't really understand why the Rust designers didn't feel that adding context to errors would have been a much better built-in macro than just bubbling up the error with 0 info about the path.


tbh the quality of default information (and lack of ability to ignore by accident) is why I'm mostly a fan of exceptions. In practice more than in principle. The vast majority of code just doesn't add enough context when it has to be done by hand.

What I think I really want is Rust-like with compiler-added return-traces by default, unless explicitly opted out (in code or in build time config), and you can also add extra info if you want.


Yes, I'm in exactly the same boat. In practice, in the code bases I worked in, the ones with exceptions tended to have the most useful debug information available in logs.

It's absolutely possible to beat exceptions with manual context, but I've only really seen that in areas of code that were actually causing problems (so people actually put in the work to make the logs useful). When a problem appears in production in an area of code that had not been causing problems in QA is when you're typically left with no useful info in logs.


> forced to choose between "throws Exception" and a runtime exception,

Not that it fully defangs the issue, but there are two other options that come to mind.

1) suppress the exception (maybe with a log)

2) capture the fact (and maybe content) of the exception and expose it through the API in some other way

Probably neither of these is going to be appropriate for most cases, but seem worth surfacing because 1 is often chosen even when it's inappropriate and 2 is often overlooked as an option.


I do not see, how this is a _huge_ problem, though. Maybe a nuisance. I tend to lay out my map function anyways, as i have to test those. And there i can have my error handling right away and return an optional or a record imitating maybe/eitheror. Annoyed i was with IOexception, from which i can not recover anyways, but this has long been addressed by introducing UncheckedIOException.

Nah, i am happy with Java. At least in my domain.


I was very happy with Java-the-language as well, back when I worked with it. Calling this "a huge problem" is of course an exaggeration - but it is very much a regular annoyance and it is one almost inherent to checked exceptions.

I also don't agree that IOException should not have been checked. If there is any purpose for checked exceptions at all, than IOException is the most important one to check. It is the quintessential example of a good use of exceptions. It is also the one for which it most likely that the code can automatically recover - you can retry the operation, use a fallback file/address/DNS server, you can use a default value if some config file is missing, etc. I don't think there is any other clear catgeory of errors that is more likely to be usefully handled than IOException.


My take is more that functional style really wants everything to be a value. And checked exceptions break that pretty heavily. Same reason most functional style heavy languages don't have statements as much as they do expressions.

For places it gets really intrusive, consider that when/where an exception is raised in a map statement.

There is also the annoyance of how junior programmers will fixate on trying to model every exception thinking that means they have handled them. With many exceptions being necessarily delivered all the way to the user, granular modeling of them is of rapidly diminishing value compared to quickly getting it to the user in the first place.


Without parametric types, your methods grew longer and longer lists in the "throws" clause. It was easy to miss one of the dozen exceptions and only have the compiler point you at it after wasting several minutes on compilation (think 133MHz CPUs and slow HDDs).

Type inference in modern Java makes you forget how long and repetitive type signatures used to be in it 20 years ago.

BTW this makes Result<D, E> more ergonomic anyway: you still have only one E, at least one per invocation level. With checked exceptions, you had to haul a whole garland.


> It was easy to miss one of the dozen exceptions and only have the compiler point you at it

This sounds like (one of?) the killer feature of types, though.

> after wasting several minutes on compilation (think 133MHz CPUs and slow HDDs).

Sure, but everything we did was slow then.


I don't remember Java being that slow even on a 133MHz CPU. Maybe if it was a giant project, though.


A large project, and a poor ant config %) Also, small RAM.

Incremental changes were pretty fast though. (Then IDEA came.)


One of the problems is that return types and exception types are not unified so it makes composition harder. `Result` brings exceptions into the regular type system


Thanks to everyone who replied. I'm responding to my top-level comment rather than to everyone individually.

It sounds like the objection is not to the fact that the exception type is checked, but that "checkedness" doesn't fit in well with other aspects of the language. For example, you can't be polymorphic over it, or abstract over it.


I'll let you in on my cynical take. Warning: it's both cynical and a bit condescending.

The truth is that programmers are just as susceptible to fads, cargo cults, and appeals to authority as anyone else. Around the same time that everyone decided that checked exceptions were horrible, programmers were also deciding that they didn't like static typing at all and a great many were jumping to Python and JavaScript for everything.

Now, the pendulum has swung the other way, and everyone likes static typing, and they'll even go so far as to proclaim their love for Rust's Result type, which is the same damn thing as checked exceptions in practice (with extremely similar pros and cons and ergonomics). But, they haven't yet gotten around to questioning their belief that checked exceptions are axiomatically bad, so you'll see people point out every minute, trivial, difference between Java's checked exceptions and Rust's Result type in an attempt to rationalize why they like one and not the other. Some will also claim that Rust doesn't have unchecked exceptions, just because you can set a compile-time option to abort on what Rust calls "panics".


Checked exceptions in Java are not first-class as values or types. They don't behave like anything else in the language and you can't meaningfully abstract over them. Combine this with a bunch of boilerplate needed to handle them and you have unescapably painful APIs.


> I never understood the objection to checked exceptions.

Any non-trivial code tree will throw an unmanageable number of exceptions. The solution, in Java, is to catch exceptions and rethrow them as a new exception with the previous exception squirrelled away untyped within it. So either way, it's a failure.

It also doesn't work with higher-order functions.

It's also, in my opinion, conflicts with polymorphism. If you have a interface that can be implemented many different ways, you can't possibly have correctly typed checked exceptions for it.


Disagree. Semantically if you are wrapping an error it means you that you are purposely shifting the responsibility and declaring that your function is responsible for the failure. If you instead re-throw the error, such as if was an IOException, semantically it means that you are not responsible for the error and that the calling code should deal it. If later on you need to add a new thrown exception, it does result in a breaking change on upgrade but it should, as new errors should be thoughtfully handled.

The real culprit is that most people realize that they are not building rockets and thus think that errors, recovery from error and mitigating data loss are not worth their time. Just because I’m not writing a rocket schematic does not mean it’s okay that you lose my 2 hours of work, or that PagerDuty alerts go off at 2am because there was one blip and the database is now slightly corrupted, or you put my account into an undefined state where I have to beg support for help. If errors are checked, many people just wrap all errors will-nilly. If errors are not checked, they never bother with sensible error handling.

To me, whether a language has checked errors or not is just feature of the tool, and whether someone uses tools correctly or not is up to the user.


> Semantically if you are wrapping an error it means you that you are purposely shifting the responsibility and declaring that your function is responsible for the failure.

I don't understand what this means. If you wrap an exception and throw it, how has your function become responsible for the failure?

> If errors are not checked, they never bother with sensible error handling.

My most robust desktop application has a single exception handler at the event loop that puts the exception message in a messagebox. Try and save your file to a network location that doesn't exist anymore? Click OK and try again.

> To me, whether a language has checked errors or not is just feature of the tool

You didn't address the criticism that checked exceptions are fundamentally incompatible with other features such as higher order functions and polymorphism. It's an experiment that has ultimately failed in practice.


> I don't understand what this means. If you wrap an exception and throw it, how has your function become responsible for the failure?

That’s a declaration that this specific kind of problem (say, an IOException) can be handled as this other, higher-level exception (say, of type MyApplicationException, that when bubbles up, will display a friendly error dialog to the user, and logs the underlying IOException for the devs).

> checked exceptions are fundamentally incompatible with [..] higher order functions and polymorphism

They aren’t. Look into languages with algebraic effects.


Maybe I know how to handle IOException but not MyApplicationException but the method only gives me the latter. There is effectively no handling MyApplicationException so why even force the declaration. Either way most exceptions cannot be handled in any meaningful way anyway so checked exceptions are pointless busywork.

> They aren’t. Look into languages with algebraic effects.

Can you give me more to go on than that?


Weak type system made it incompatible with function types in practice, and poor compositional tools made it hard to deal with.

In Rust, you have entire libraries around the ergonomics and management of errors to match different applications. In Java, you are stuck using an error management strategy dictated by the error type itself.


Java is perfectly capable of expressing this thought, it has sum types, so the Result-type road is absolutely available to it. (Optional<T>=Maybe T is part of the JDK even) Although, it might not optimize as well, as this is not common practice.


Not all exceptions are checked. This kind of ruins the whole point. IllegalArgumentException for example is very common and would be what you’d use for a bad input to something like new URL(). So it’s a very common exception to see, maybe second only to the NPE, and yet it won’t be checked. What the fuck is the point then?


No. It’s hated by the majority because it shows a problem which should be handled, but most developers don’t want to care about exception handling, and because of this they make terrible code. They would make bad code even without checked exceptions. Checked exceptions just make it obvious.

However, when high profile people criticise it, they criticise it because, for example, interfaces work terribly with checked exceptions. There are problems with collections for example. Like when filesystem backs a list, then that list cannot throw IOException. This is why there is RuntimeIOException now.


Checked exceptions are only hated because java historically hasn’t provided enough syntactic sugar for it, basing it on inheritance is not the best decision and due to it being sort of outside java’s general type system. (Note: Java is thinking about making switch expressions able to handle exception cases as well, which would help tremendously)

I think the backlash against them was way too hard to the point that basically no other mainstream language dared trying them out, even though exceptions in general are - in my slightly subjective opinion - the best form of error handling, paired with some static guarantees sounds like an optimum. They do the correct thing by default (auto-unwrap in the happy case, auto-bubble up with a stacktrace in the unhappy one), and let’s one control their handling on as wide or narrow scope as required (try blocks). Nonetheless, I think that they are to be used for exceptional situations, like network interrupts, OS errors, etc. Expected error conditions, like parsing a number failing is best encoded as a sum type, analogously to an Either/Result type.

Newer research languages with effect types might resurface this “lost” idea, I’m definitely looking forward to them.


Swift does this well. People who object to checked errors probably don’t have a good working mental model of error handling, and just want to write lower-quality code that happens to work. Ergonomics are secondary to that in all cases, IMO.


They are hated by some vocal people, who do not like that they have to actually deal with exceptions rather then ignoring them, fingers crossed it wont happen.


Zig's exception handling is also, similarly, quite nice.


Ahh... MalformedURLException is checked in Java and is a source of constant backlash from developers - for over 20 years :)


I prefer the Optional.of and Optional.ofNullable approach. Analogous there should be URL.of and URL.ofChecked, the latter for potentially unkown input. Same for Charset.


malformed urls have always been the issue, not the exception


There is also no way to specify in the contract of the function that it can return null. Blowing up your entire program because it can't parse a URL and returns you a booby-trapped value that blows up your program when you try to use it as an URL is something you should be made explicitly aware of. Right? Although there are docs, of course, but who has time to read them? Or write them, for that matter? For some reason many client programmers are perfectly fine with calling random functions that has names sounding close enough and then expecting those functions to behave the way they expect them to behave, and many library programmers don't bother to document the actual behaviour of their functions because they for some reason believe that descriptive and self-evident (well, self-evident to them) names are enough of documentation.

And you know, I think I can safely bet $5 the way TFA came to be was something like this: the author wrote a code that used URL() with no error recovery-path whatsoever, ran it, and it blew up on that line. The author then rolled its eyes, wrote an (admittedly) ugly try-catch around, and went to write the article complaining about exception.

Now, I can bet another $5 that if URL() returned null what would happen instead would be that the author would write a code that used URL() with no error recovery-path whatsoever, would run it, and it would blow up somewhere down the line, on call to fetch() or something. The author would then ponder the code for several minutes, trying to figure out what exactly fetch() didn't like about the URL it was provided with, find that null came from the call to URL() and add the error recovery there. Crucially, they later would not go and write a article complaining about null returns because they regard it as a completely normal and acceptable behaviour.


This is one of the reasons for the broad popularity of TypeScript, where “[specifying] in the contract of the function that it can return null” is foundational to the experience. Meanwhile thrown errors are unmodelable COMEFROM statements in TS and many other languages.


Why is a public constructor with a string an anti pattern? 99% of the use case is parsing a string, and 99% of the users are going to try using the constructor first.


Swift, and I imagine other modern languages, do this the right way. Their idiomatic error passing is via try/catch, which is the best error handling pattern we've figured out so far, but instead of bloating the API for every case where somebody might theoretically not care about the specific error and isn't doing multiple failable operations in close proximity, it just adds some sugar to get the result described in this article:

`let fooOrNil = try? Foo("blah")`

Edit: Updated URL to Foo, since Swift happens to use optional return for its own 'URL' type, as pointed out.


> Their idiomatic error passing is via try/catch, which is the best error handling pattern we've figured out so far, ...

I find monadic error handling (`Result<T, E>` and `Option<T>`, or `Either<T, U>` and `Maybe<T>` if you prefer Haskell) to be much better at keeping error handling logic local to where errors originate, and making it explicit which operations are fallible and what errors they produce. Exceptions tend to be nonlocal and difficult to determine what exceptions can occur from where.

That sugar for getting an optional/nullable value with `try?` definitely looks nice, I wish Python had something like that, but it seems to me that the reason it works better is that it's closer to monadic error handling (you get an `Option<T>`).

Has your experience been different?


Swift’s `try` isn’t “real” exceptions. It’s basically syntax sugar around a Result type. You can convert them trivially:

    func someThrowingFunc() throws -> T
    
    let someResult = Result(catching: { try someThrowingFunc() }) // someResult: Result<T, any Error>
Or the other way:

    let someResultFunc() -> Result<T, any Error>

    let t = try someResultFunc().get() // t: T
The reason I say they’re not “real” exceptions is that it compiles down to a branch that checks if a particular error return register is non-nil, same as you would with a result type. (As opposed to “real” exceptions like in C++ where execution is forcefully interrupted by an expensive exception handler.)

I started out in swift treating it like rust and using Result everywhere, and manually chaining results back up when I didn’t want to handle them, but after a while I realized that the `throws` and `try` statements were just way more readable syntax to accomplish the same thing I was already doing:

Want to “not care” about the thrown error? Use `try?`. Want to propagate it up? Use `try` and just mark your function as throws rather than explicitly returning a Result. Want to crash on an error? Use `try!`. Want to explicitly deal with the error? Use `catch`, or if you don’t like that syntax and you’d prefer matching on a success/error enum you can just convert it to a Result as I outlined above.


> Exceptions tend to be nonlocal and difficult to determine what exceptions can occur from where.

This is true statically (mitigated by checked exceptions in Java, but that's a whole can of worms on its own), but the opposite is true when you actually have an error at runtime—exceptions come bundled with stack traces which make it very easy to see the stack of function calls that get us into the failure state, where with result types you at best have a (hopefully descriptive) error message and that's it.

If the code has already been well polished then that error message is probably enough, but if you're still ironing out rough edges the error message itself may be in error and the actual problem may be somewhere else entirely. With a stack trace you can easily see the function calls that got into that unexpected state, but without them you're on your own and have to try to recreate the error while stepping through in the debugger.


But with a `Result` type, I'm forced to deal with each error case at compile time, so I avoid finding out about an unexpected edge case at runtime in the first place!

(My perspective is Rust centric, and if you eg unwrap an error because you mistakenly thought it shouldn't be reachable, you end up with a panic which is just an exception.)


> so I avoid finding out about an unexpected edge case at runtime in the first place!

This has not been my experience with Rust. In my experience I'll handle all the errors just fine—nothing is unexpectedly crashing—but then every once in a while an error object will propagate up and I'll have no clue why that error got triggered by that input. If I'm lucky I'm working on something that makes dropping into the debugger easy, but other times this is the beginning of a very long troubleshooting session that would have been much shorter if I had a stack trace.

Mind you, this always does turn out to be a mistake in my code that caused an error to get returned incorrectly, but Rust doesn't help me find and fix these problems the way that Java does.

(My personal opinion is that Java's checked exceptions were on the right track and just need some UX improvements and syntactic sugar for handling them. They provide all the safety of a result type with all the debugability of an exception.)


> (My personal opinion is that Java's checked exceptions were on the right track and just need some UX improvements and syntactic sugar for handling them. They provide all the safety of a result type with all the debugability of an exception.)

Except that it does not matter whether exception is checked when reflection is used. All you get is unchecked InvocationTargetException that wraps the actual exception.

To the best of my knowledge method signatures in JVM do not contain declarations of thrown exceptions, that's Java thing. Java being Java, all the sparkly modularization and stuff means that effectively calls to entrypoint methods are hidden behind `invoke()` anyway. Or compiled against binary libraries.

Checked exceptions only work as supposed to only under very specific circumstances, usually don't and that is by design - Java/JVM features do not interact with each other nicely.


Heh? Yeah, and a C FFI exposed Rust function will also not care about, well, anything, let alone whether you handle the error case or not.

I don’t see your point. Reflection is not the primary way of accessing methods, it’s used similarishly to unsafe in Rust, you have a safe wrapper around them. Its exception type pretty much means that you knowingly entered here, proceed with care.

As for JVM-interop, sure, checked exceptions themselves are only a java compile-time feature. I don’t really see it coming up all that much though, you can easily wrap in some language-specific way, but java can be much more lenient about error cases like this — you can at worst just do a catch-all at the top layer, and restart the operation. The VM can’t go off the happy path in any case. This is unlike rust’s unsafe, for example, which only needs a single unintended data race, and afterwards all bets are off.


Huh, interesting. So is this a situation where a library uses one error type for everything, so there are several function calls which could have triggered it, or where the documentation is lacking so you know where it was triggered but lack context? Or something I'm failing to imagine?

ETA: Rereading I see you meant that an error was returned from the wrong code path. That is interesting, and I do see why you're lamenting the lack of a stack trace in that situation. Maybe there's some way to write a library like `thiserror` that includes a lightweight, application level "breadcrumb trail" that's cheaper than a stack trace but with some of the observability improvement, I'm not sure. Maybe I'm just reinventing logging.


That's true and something I've run into as well. It does seem like there should be an easier way of getting a stack trace... or at the very least a line number.


I’m confused, wouldn’t RUST_BACKTRACE=1 effectively give you a stack trace?


I think the scenario here is that a function unexpectedly returns an Err(...), but the program propagates it several frames upward before finally unwrapping it, which produces only a partial stack trace. This can be mitigated with error types like anyhow::Error which remember the originating stack trace, but such types aren't in common use in the library ecosystem.


The exact same way as in case of a checked exception.


>making it explicit which operations are fallible and what errors they produce

I should specify that throwability needs to be part of the function signature to be useful in the language. As for what types of errors: At minimum they should provide something you can show a user, maybe after adding context, and the machine-readable codes should be documented, both of which are points of failure, but the ability is there.

Interestingly, Swift is getting typed exceptions, which seems too good to be true. I'm not sure how it works when passing exceptions up the call chain.

As for Optional/Maybe, as I replied to a sibling, that's not really comparable. It's not for error-handling because it doesn't provide an error. It's useful for when something might not exist, but not so much for when some input is invalid, etc, since it's more verbose to explicitly fork on null and return early.

As for Result<T,E>, it has a subset of the same problems: Can't pass it up without writing a fork/switch unless it's just a wrapper function that returns Result<T,E>. And it makes things less composable or require extra forks when something is expecting T. But, like Optional, it definitely has uses: Like if you're using a callback system for an async function, where throwing is impossible.


I agree that exceptions are much more tolerable when they're part of the method signature. Otherwise you have to dig through the code to figure them out, and that's a mess.

You can think of `Optional<T>` as being `Result<T, null>` - it's a fallible operation where there is either one obvious way it can fail or where we don't care to distinguish between them. If you look a key up in a map, that's a fallible operation, but there's only one way for it to fail - the key wasn't present in the dictionary. It's definitely error handling.

`Result` really comes into it's own when combined with pattern matching (`switch` with destructuring and exhaustiveness, so compilation will fail if you've neglected to handle a certain error case (!!!)). Rust adds syntactic sugar and other features that make this very ergonomic. Eg, there's a `?` operator that will return early if there is an error, and traits (interfaces) that make it seamless to convert one error into another, add error messages, etc.

> it makes things less composable

Not so! They're very composable, eg:

    let x = my_fallible_operation().map(|v| do_something_on_success(v)).or(other_fallible_operation_to_compose_with())
If `my_fallible_operation()` success, `x` will be `Ok(do_something_on_success(v))`; otherwise, it will be the return value of `other_fallible_operation_to_compose_with()` (itself a result).


> As for Result<T,E>, it has a subset of the same problems: Can't pass it up without writing a fork/switch unless it's just a wrapper function that returns Result<T,E>. And it makes things less composable or require extra forks when something is expecting T. But, like Optional, it definitely has uses: Like if you're using a callback system for an async function, where throwing is impossible.

Are you saying that Result<T, E1> doesn't convert to Result<T, E2> automatically? Cause I've had great success implementing `From<E1> for E2` and forwarding the error via `?`. There's also map and unwrap helpers to convert. If you're saying that Result<T, E> is incompatible with a return type of T, that's often seen as a benefit because they are incompatible and you have to choose how to bridge that gap by for example unwrapping because you don't expect an error (equivalent to not specifying a catch and letting the thread terminate) or somehow else doing something with that error (specifying a catch handler). What it doesn't let you do is ignore the failure case silently and implicitly.


I never really understood this “exceptions are non-local” stuff. They are basically the same as the sum-typed approach, with sane defaults. They are just “early” returns, that go to the callee, and either go to a local handler logic, or just early return from there as well.

As for sum types, it is definitely not good practice, but I would say that if you fail to handle an error condition close to its source, and it managed to get to some “global” scope, determining where it originates is very hard. Similar to that illegal database row that somehow managed to get entered, resulting in a difficult investigation.


The try/catch is basically a different stack, and a single try/catch frame may be many stack frames. I've found that for complex stuff I have to walk up and down my abstractions to understand how exceptions are flowing, and that it isn't easy.

I think it's fundamentally less local to do a conditional jump elsewhere in the code (even if doesn't cross into a different frame) than to return a value and step to the next line, rain or shine. I don't disagree that there's an isomorphism between the approaches (since they're both addressing the same need, surely there should be), but exceptions introduce a new set of rules whereas monadic error handling repurposes the familiar semantics of function calls.

But you're not the first person to mention your second point, so I totally concede that, regardless of whether it is "bad practice," empirically it's a weakness of the approach.


You're assuming that error handling logic should be local, but I don't think that's ideal. Most of the time, I really do just want things to bail out if something goes really wrong and log what happened. This is because what often happens is that the real error occurred a while back and so the code expected to handle the error has no idea on how to do that (like passing in a mangled URL).


Probably most of the time you just want to crash, but that's a pretty easy requirement to satisfy. I don't think either approach has a particular advantage there.


Nonlocality of exceptions is a feature to me. I don’t want error handling logic all throughout my applications, I want it only in the contexts I care about failure.


Well, we want the same thing, I guess we have opposite ideas about how to achieve it.


> which is the best error handling pattern we've figured out so far

What are the reasons you think it's better than Either and map, especially for expected control flow that doesn't signify programmer error and just responds to bad input data? I can't see any upsides. The function doesn't force the caller to handle the error, and the error isn't type checked: if the function stops throwing one type of exception or starts throwing another, type system won't force outdated call sites to be fixed.


That's arguably a poor use of exceptions. Swift has URL.init which returns nil if parsing fails. It doesn't throw.


I only meant to illustrate the feature, not argue that this specific constructor should use exceptions vs optional return.

Regardless: Why would it be a bad use of exceptions? It reduces the API size and unifies flows, and gives me the option of seeing the specific error. It simply happens to be that in practice I've never needed to know the specific error in this case.


In .NET it’s idiomatic to avoid exceptions for control flow because “it failed to parse” means creating an exception which costs you an allocation and getting the stack trace, among other things.


    > means creating an exception which costs you an allocation
Please. What percent of .NET users are really worried about that allocation? Maybe 0.01%?


Exceptions are meant to represent exceptional states.

The data is always expected to be valid and there is no recovery? .Parse

The data may be invalid and the implementation can handle it? .TryParse

This is a very common pattern and adopted for reasons besides the cost of exceptions. Though in this case it is Uri.TryCreate instead.


The allocation is plenty cheap, but creating the stack trace is expensive in pretty much all structured exception systems, and it is noticeable in loops, not necessarily even really tight ones.

So yeah if you’re parsing thousands of url candidates and expect many will not parse, don’t use exceptions to communicate the expected errors. Otherwise don’t worry about it.


Exceptions are nice DX, but theoretically if you were parsing URLs in an inner loop and expecting lots of them to fail it would be very expensive compared to returning a tuple (or Optional, whatever).


If it doesn't return an error code, nor throw an exception, how do you know why it failed? Man, the resistance to exceptions with their human-readable error messages and stack traces and sorely misunderstood on HN. Any time someone says "exception", 50 people show up to point at the boogey man. Why all the fear? I grew up programming in the era of "errno only" (or god forbid: Win32's GetLastError()). It was awful. Exceptions are so much more human-friendly. Most programmers are writing CRUD apps for a business. Exceptions are terrific for this case.


> Man, the resistance to exceptions with their human-readable error messages and stack traces and sorely misunderstood on HN.

You grew up with "errno only", so that's your frame of reference. You've seen exceptions and don't want to go back to errno. Do you "fear" errno? Is errno the "boogey man"?

I've seen monadic error handling and I don't want to go back to exceptions.


There's a range of other options for error handling between a global errno and wrapping every lines in try-catch...

Many languages make it idiomatic for functions to return a value or error (Rust, Go, modern C++) so that you can access a human-friendly error on failure.

https://doc.rust-lang.org/std/result/

https://go.dev/doc/tutorial/handle-errors

https://en.cppreference.com/w/cpp/utility/expected (there's also std::variant and std::optional that can get you there since C++17)


Go pretty much uses a global errno, it’s literally analogous, and is a terrible language from an error handling perspective, that failed to learn anything since C.


Blanket update:

I have been referring to try/catch error-handling, but have used the word "exception" as a synonym a couple times, and I think a couple other people have made the same assumption of synonymity in their replies.

Really, "exception" should be reserved to refer to things that "shouldn't happen", that we don't want to waste resources recovering from. In most languages, try/catch is exclusively for exceptions, and they are sometimes/always unchecked, but in Swift, try/catch is for plain errors, and the compiler forces you to handle them. This is a huge caveat, and a mistake on my part, that makes 50% of the conversation moot. I am only lauding try/catch error-handling, and checked i.e. enforced try/catch at that, but not exceptions.


I think I agree on that, but returning null seems like a bad idea in this specific instance. Instead, I think I'd prefer they return an object like a promise so people are forced to check if an error occurred.


I much prefer Maybe types to try/catch.


They're not really comparable. They serve different purposes. Maybe/Optional is for when something might not be, while try/catch is a powerful system for passing both user-facing and machine-readable information about errors across potentially multiple layers of API. A powerful language has both.

Saying "I prefer Maybe to try/catch" is too broad - it's almost like saying you prefer not to handle errors.


You have this exactly backwards as far as I am concerned. Exceptions might seem like they force you to handle errors, but in practice they force you to, at some level, not handle them. I've never seen a serious, production-serving tome of code that doesn't end up generically catching all exceptions at the top and logging them blindly because all context has been lost.

If you were forced to handle exceptions at every call site I might find myself able to agree, but then you have something functionally equivalent to options. I'd much rather just start with that approach.


No, Exceptions would be equivalent not to Options, but to Either where the left or right (depending on which tradition you follow) side is the error (which people normally have a special name for, Result). If you just use Optional then you cannot know why the operation failed, which is extremely important to any system as the error may not be recoverable and you need to report it properly, not just say "operation failed".


Extend the Maybe to an Either, or an Result<T, E>, that carries either the result or the error information and you have the same thing. Add some syntactic sugar to either get the success result or propagate the error (under the condition that the current function returns a Result and there is a way to cast the inner error type into the outer error type) and you have a nice alternative to try/catch that can entirely replace it


They might have meant a type like `Result<T,E>`


This is what I meant!


Result (aka Either) and Maybe are much better than exceptions for parsing. Exceptions should only be used when something truly exceptional occurs (e.g. out of disk space, no network connection). Ill-formed input is not exceptional when you're parsing a URL.


Well, you handle errors by needing to explicitly handle them at the point of error, as opposed to all callers needing to anticipate and understand every random system error from 10 levels deep of APIs (or just throw them all away).


Except this isn't my experience of errors at all.

My experience of errors is that most of the time you need more scope in order to handle them properly.

I.e. if writing to a file happens, what am I actually supposed to do about that? Well, that depends entirely on why I'm writing to a file. Which might not be information the calling function actually has. Usually I need to go up the call stack to find somewhere which has enough context to know what's actually not working because a file write has failed.

Basically, the vast majority of my code can always error or depends on things which can: or is one refactor away from now needing to return errors. If just about everything is Result<T>, then really why should anything be? Why eat the syntactic noise?


Of course you think the majority of your code can error - exceptions don't let you draw a distinction between fallible and infallible code, so you have to assume all code can fail.

This leads to two problems:

1. You can't take advantage of infallible code...

Infallible code is much easier to use because there is no need to worry about errors. But you can't take advantage of that because infallibility is not encoded in any way.

With Result types, infallible code is clearly marked, which allows for sectioning it off from fallible code. Programmers naturally gravitate towards doing just that because they want to take advantage of the ease of use.

The result is a lot of pure functions and as little fallible code as possible.

2. ...but you will erroneously try to do so anyway

You will inevitably start assuming that certain code can't possibly fail. The ease of use of infallible code is just too alluring: [1] [2] [3].

[1]: https://nedbatchelder.com//blog/202001/bug_915_solved.html

[2]: https://devblogs.microsoft.com/oldnewthing/20050114-00/?p=36...

[3]: (a little wordy) https://www.joelonsoftware.com/2005/05/11/making-wrong-code-...


I don't care about code which "can't error", because it can't error.

The only code I care about is code which can error, or worse, code which is expanding due to a requirements change and is about to make the transition to "needs to be able to return errors".

There are vanishingly few business logic processes that can't error, because every bit of disk IO, network IO and user input can always throw an error that needs to be handled.

What I am saying is that the benefits of pure functions are obvious, but the number of practical applications of them is far smaller then their advocates claim. They don't save me from the fact that the network may be down, the target device might crash, someone might knock the power out at the other end, or the network card picks that moment to corrupt a packet.

What is the point of spending a lot of syntax on Result<T> types returning all over the place, rather then just throwing a try-except around the places in my code I know I can reasonably bound and define what the recovery process is if an error does occur. Which for every non-toy example, is a guarantee.


Infallible code is too rare to be worth any effort. Even math can fail (though most platforms are stuck with legacy decisions to quietly return wrong answers). And you always have to recover from a timeout due to a downstream failure in some of the cheap commodity hardware that's running your code.


I tend to agree with you. Rust uses Result types and for any real-world API, you can be almost certain that 90% of operations can fail and need to return Result. You can see that in most Rust code that's not just algorithms, you will have a `?` at the end of nearly every method (which is the way Rust propagates errors, similar to how Exceptions are automatically propagated in most languages - but they don't require the `?` to mark them).

Rust kind of cheated a bit with arithmetic and it doesn't return a Result when you use division, for example (it's cheating because if not for convenience, it really should return Result as arithmetic errors like division by zero and reaching a NaN are fairly common - and hence would justify that).


Not only are they comparable, they're so comparable that one ought to put them under the same interface.

If you write library code, fail polymorphically, then the client can choose whether they want to call it as Maybe/Either/Exception and we can stop these never-ending arguments.


They are not quite orthogonal but at least independent.

Maybe/Either/Result is a way to represent partial computations at runtime.

try/catch is a syntax to detect partial computations and to do something conditionally if one occurs.

You could have a language that used try/catch syntax to catch Result types, indeed Rust does something like this with the try operator although it’s only try/catch if you squint very hard.


You can do something conditionally with the monadic types too; you branch/match on them.


Yes, but what I’m saying is that you can also handle monadic types using try/catch syntax. Consider this imaginary language, the bastard child of Rust and Swift:

    fn returnsAResult() -> Result<i8, Error> {
      return rand(0,1) ? Ok(4) : Err(BadLuckError)
    }
    
    do {
      let x = try returnsAResult()
    } catch e {
      print(e)
    }
This language doesn’t exist, but it could.

Results are in-band errors where the error is a normal return value. Swift by contrast uses out-of-band errors.

Try/catch is one particular syntax for detecting errors. Match syntax is another. In principle there’s nothing preventing match syntax being used with out-of-band errors, even with Java-like exceptions. It would look something like this:

    match (iCanThrow()) {
      case 42 -> …
      catch e -> …
      _ -> …
    }
I like this one less than the first one though.


> error passing is via try/catch, which is the best error handling pattern we've figured out so far

The best error handling pattern for scripts we've figured out so far, but at the same time the worst error handling pattern for systems known to man.


I disagree. I write very large code bases in Kotlin and Java. Kotlin did away with checked Exceptions and it's a pleasure to use, without any significant decrease in the quality of the systems compared to Java, quite the opposite because Kotlin forces you to check stuff like nullability. I've also used Rust and I simply can't see any real improvement in the reliability of code I've written just because it forces you to use Result type (it's just too easy, and tempting , to swallow an Error or panic with `unwrap` because otherwise you just end up with every single function returning Result, making it really non-egornomical to work with... arguably the Rust language itself did that with arithmetic operators).


It seems you agree. Systems and scripts are not differentiated by the size of the codebase. The way you speak about errors, like "being tempted to swallow errors" and "returning Result from every single function", suggests that you are accustomed to writing scripts. Which isn't unexpected as a lot of computing workloads are script in nature. And, indeed, the 'try/catch' model is well suited to it.

But it not fun when writing systems. That is why we have different tools for different jobs.


Your definition of "script" is completely different from what most people would use and honestly makes no sense.


That's fine. Obviously words can mean anything and naturally one would ensure that they are aware of what they are intended to mean in a given context before going any further if there is any lack of clarity. As there was no earlier questioning of what they might mean, there clearly was no issue with clarity. To backtrack now is strange and hilarious.

But I posit that the "definition" is consistent with the literature. Scripts carry out a defined set of tasks and if something goes wrong they can simply fail, wait for the problem to be resolved, and then try again. Systems don't have the same luxury. They have to find alternative solutions when fault occurs. "An error occurred. Try again later." is not acceptable for systems, but is acceptable and perfectly valid for a large class of computing problems.

Again, "try/catch" is great for scripts as most of the time you just want the error to "bubble up to the top" to notify the user, or whatever, anyway. Such a thing would be unthinkable in systems, though. When you actually have to deal with errors at every turn, "try/catch" is a slog, and I would argue a poor mental model to boot (what's special about errors that deserves something different?).

It turns out that software development is an engineering endeavour, not a religious one. Different tools for different jobs.


I suppose you live in a bubble where "script" is almost all code ever written. Here's a definition of "script" that shows how out-of-touch your definition is: https://en.wikipedia.org/wiki/Scripting_language

"... a script is a relatively short and simple set of instructions that typically automate an otherwise manual process."

You keep talking about "system" as if there was also a clear definition for that in software, which makes me think you're not even a software engineer at all, are you?

> Obviously words can mean anything

No, that's your mistake. Words can't mean what you choose they to mean! Do you know the old meme "Stop trying to make fetch happen!"?? So, "system" wont' happen!

I guess that you're talking about embedded development??? Where Exceptions are not used because they're a bit too costly (and because they're written in C which has an incredibly primitive error handling, it doesn't use Result types anywhere), NOT because they make software unreliable. You may disagree with that, but for goodness sake stop with your condescending tone pretending that you know something you clearly don't and work on "systems" which we, mere mortals (despite you having zero idea what kind of "systems" I work on), have no idea about.


> I suppose you live in a bubble where "script" is almost all code ever written.

Most tasks humans perform are in following a "script" so stands to reason that software would carry that forward. Why wouldn't most code be of the script variety? It would be quite surprising if that wasn't the case.

> which makes me think you're not even a software engineer at all, are you?

Appeal to authority and ad hominem at the same time. Intriguing.

> I guess that you're talking about embedded development???

I am talking about scripts and systems. You had this figured out earlier when you linked to Wikipedia. It was right there in front of you. Why the confusion again all of a sudden?

It is probably true that embedded device work is generally system in nature. It is likely that their typical workload isn't amenable to scripts, although there are some obvious exceptions, so perhaps that is what you had in mind there? But systems certainly aren't limited to embedded work.

> Where Exceptions are not used because they're a bit too costly

While I respect your ability define words as you see fit, if you really want to be the word pedant, we're talking about errors, not exceptions. Specifically carrying errors via exception handlers. It is not unusual for embedded systems to store exceptions, even in C, but that is well beyond the topic at hand.

It is always funny when the pedants aren't even technically correct. Well, at least you tried. You will always have that.


I kinda agree that `new URL()` need not bail out when the URL is invalid. Both practices exist in the spec: `new Date('foo')` returns invalid date, `parseInt('foo')` returns NaN, while `new Array(-1)` throws a `RangeError`. Probably there is a need of URL instances for invalid ones? Then we come back at an Either<x, y> return type.

However, it is the `try...catch` pattern that messes up with the `const`, not the URL constructor. It is very annoying every time when I have to wrap an existing block with a try...catch, and inevitably lose the const-ness to some variables, unless I wrap everything again into a function and `return` in the try block if things go normally.


Re: try-catch x const, this is indeed one of top annoyances in JS those days. I hope this proposal makes it to the language one day:

https://github.com/nzakas/proposal-write-once-const?tab=read...


Thanks for the reference. I am surprised that Java already has a solution and the choice of keyword "final" is a nice one.


I often find myself using an immediately invoked function expression to handle the try catch const assignment issue, for example:

  const result = (() => {
    try {
      return new URL(someString)
    } catch {
      return null
    }
  })()
It's not pretty though, and I think the do expressions proposal would rid my codebases of this pattern: https://github.com/tc39/proposal-do-expressions


Thanks for this, I think your syntax is an improvement over the solutions I've tried


try/catch messing with const is a well known problem in modern JS & one that imo needs a generalised solution & not one specific to one constructor.

As far as other error return patterns existing in "the spec" - there are multiple specs referred to here (ECMA vs DOM) & also multiple eras; there seems to be consensus that some past patterns on error return weren't great decisions & some well considered consensus to move to more broadly accepted known patterns (like try/catch).

e.g. for your Date example, that's a really old API that ECMA are working on replacing with Temporal (which throws)


just make a utility function! don't see what the fuss is about. the original API choice is arguably not great, but haphazardly adding multiple ways to do the same thing does not improve the API either.


Especially because we have npm packages for things like left-pad.

So this is a no-brainer.


Something like...

if (typeof URL.parse != 'function') {URL.parse = function (_s){let _ret; try { _ret = new URL(_s)} catch {}; return _ret} } ;


Yeah, you can write it in less than a minute:

const tryNew = (f, ...a) => { try { return new f(...a); } catch(x) { return undefined; } };

const myUrl = tryNew(URL, 'http://example.com/');

I don't get why JS devs like to whinge about the smallest things. And we get stuff like leftPad because of huge aversion to writing utility functions.


Ironic since your snippet is now a new leftpad.

Does every dev need to write the same line now? Or should your one-liner be a library?

The basics that everybody needs, should be standardized in the standard library.


> Does every dev need to write the same line now?

Yes.

> Or should your one-liner be a library?

Definitely not.

Particularly if I can write the one-liner faster than it takes one to search, check, include and install the library.


On the one hand, the Javascript spec is so littered with API helper functions to patch over old APIs that I think it'll only continue to grow in exploitability.

On the other hand, you don't want to be Java, where List.Last took until Java 21 to get implemented. It's not hard to make a wrapper function, but it's really annoying to clutter your code with helper functions where the native API should help you.

In this specific instance, I agree with the new spec: most constructors for native Javascript types don't throw, so neither should the URL constructor. However, the try/catch isn't exactly the problem some people seem to think it is. It's a minor annoyance that apparently annoyed at least three browser teams enough to come up with a new API. In other circumstances, I hope the API spec won't be extended as easily.


> the Javascript spec is so littered with API helper functions to patch over old APIs that I think it'll only continue to grow in exploitability

Can you explain how helpers create exploits? Are there any examples?


The higher the surface area, the higher the risk. Either browser engines maintain two separate methods for creating the URL object, or they use the same base function that's called in two different ways. If someone writes optimised code for one, they can easily forget to keep the other into account.

It's easier to secure a JIT with 100 methods than it is to secure a JIT with a thousand.


The best solution was already there: have a canparse() to do up front if statement checks, and use the language's exception handling for the rest.

I understand people might not like exceptions, but special-casing certain calls like just makes the language harder to use.


I feel like it shouldn't be the classes responsibility to provide a method to return a nullable / falsey value. We should have a convenient one-line syntax around try / catch. Something like pipe operators and or pattern matching.


It's annoying that none of these end up parsing quite like the HTML inside a website.

For example, on google.com, you can find:

<img class="lnXdpd" alt="Google" height="92" src="/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png"

But open the console and run this, and it will throw:

let x = new URL("/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png");

You're supposed to address this by adding the base URL on the end, but then correctly obtaining that can be brittle. If you're browsing a folder, the whole document.location is the baseURL, but if you are browsing a file (ie ends in .php or .html) it isn't. If you're taking user URLs, your browser's URL bar will add https:// automatically so people don't think about it, but they'll leave it out of an absolute URL and now you need to detect and add it if needed.


I actually like that it does this. In your example where you’d want it to infer relative URLs, what happens when that code is executed in Node?

> If you're browsing a folder, the whole document.location is the baseURL, but if you are browsing a file (ie ends in .php or .html) it isn't

I’m not sure I understand what you’re saying here. The second argument in the URL constructor works as a “relative to” argument. If your URL starts with / it doesn’t matter if there’s a file or not, it’ll start from the domain root.


There's either 4 or 6 combinations (depending on whether you consider the domain-only case separate from path or not):

   www.example.com                   + /images/foo.jpg -> www.example.com/images/foo.jpg
   www.example.com/static/           + /images/foo.jpg -> www.example.com/images/foo.jpg
   www.example.com/static/index.html + /images/foo.jpg -> www.example.com/images/foo.jpg
   www.example.com                   + images/foo.jpg  -> www.example.com/images/foo.jpg
   www.example.com/static/           + images/foo.jpg  -> www.example.com/static/images/foo.jpg
   www.example.com/static/index.html + images/foo.jpg  -> www.example.com/static/index.htmlimages/foo.jpg (naive and wrong)
                                                       -> www.example.com/static/images/foo.jpg (extra work)
They were referring to the difference between the 5th and 6th (probably didn't notice it was an absolute path), you're referring to the first 3 together vs the others.


Interesting, I’ve never run into that. In the last example I’d just do

   www.example.com/static/index.html + ./images/foo.jpg


`src` takes a path, not a URL.

I think you just misunderstand what a URL is.


I would think of it as a "URI-reference" (a URL or a relative ref), since `src` can be thinks like "https://example.com/test.png" (absolute URI, not a path) or "//example.com/foo.png" (relative, not a path).

The bigger point is yeah, type T isn't a parser for type U.

I guess unfortunately, I'm not sure if there is a URI-ref parser in JS that I know of … but does one ever want to do that, without first normalizing it over a base URI?


Are you saying that is the to string value of a URL object instantiated with the url of a page containing just an image tag, or are you saying it’s the value when you instantiate an object with a url parameter to the png file itself? If it’s the png file itself my 0.02 is that is a logical choice. Obviously the to string ( or whatever it is, I’m not a JS dev ) isn’t the binary data, and I believe in the binary encoded response body there is a meta data signature block that contains that info.


Don't forget to handle the case where the document overrides the base with `<base>`.


> You're supposed to address this by adding the base URL on the end, but then correctly obtaining that can be brittle.

Isn't that what document.baseURI is for? At least that's my understanding:

new URL("/whatever", document.baseURI)


I encountered another pitfall when using `new URL` to check if a text contains urls. It accepts strings like ”example:”, “next:", etc. as valid urls because it interprets the string before the colon as a custom protocol. This is a real edge case in the spec, because with common protocols, just "http:", "https:" is not accepted as a valid url.


It's correct, though. The URI scheme for HTTPS may always include a domain name, but my-app: is just as valid a URL as anything else. A protocol where everything is optional and the default opens a screen with all fields empty is valid, though unlikely.

For instance, an empty mailto: will reliably launch an email client, even if no address is specified. I think it's the second most used protocol in web links, after HTTP(S).

If you want to to URL parsing by brute forcing URL constructors, you probably don't want application URLs in the first place. You should probably start with https:// and http://, or make use of the weird double slash HTTP introduced into the URL by matching on :// instead.


Well, ackchyually...

If you want to get real anal about it the API should probably called URI instead of URL. The latter should describe where to access the resource, not just how to identify it, and to truly be able to do that you must also know something about the scheme.

At the very least you must know whether the scheme imposes further restrictions on the individual parts of the URI, otherwise you can only determine whether it's a valid URI but not truly say whether it's a valid URL.

In practice no one really cares much about the distinction between identifiers and locators, and arguably it was a mistake to try and make that distinction in the spec. But it is in there after all and I find it a bit ironic that the authors of the JS spec chose to ignore it. There's probably good reason for it, but still.

Ps. I'm real fun at parties!


Why can't we just use `Either` or ADT


You might be interested in `fp-ts` (and the related `io-ts`).

https://gcanti.github.io/fp-ts/

https://gcanti.github.io/io-ts/


I’ve never gone so far as to pull in fp-ts into a work codebase because it often doesn’t feel that it’s worth the headache of arguing for it.

However, I check back in from time to time and was recently confused as to whether or not the community has moved onto effect-ts as it seems effect consumed the fp project?

Do you have any context on that?


No sorry, I'm not tuned in to typescript at the moment. I've mostly used fp-ts in the past because I haven't found a better runtime type system than io-ts, but I haven't written typescript in a while.

Mostly responding so people with better context don't wait on me.


With TypeScript a nullable return type effectively is an ADT.


But it's not Either / Result because there no error information on null


Then return something else.


We can and do.


I don’t see how it changes anything. You’re still handling a second branch as if an error was thrown. It’s just called null.

Practically, though, I like it because JavaScript’s error handling sucks. I generally avoid using try/catch as a first class member of my software.


Exactly! Handling null is still.. exception handling. You just check it with an if, not a try catch.

But the semantics around try/catch are the main issue here and basically everywhere in JS. Error catching in JS is absolutely abysmal. Some examples:

- nested try catch

- not annotating that a function throws an error (or even being able to annotate in TS that it does)

- async code throwing an error in a callback is uncatchable. E.G. file reader onread throwing an error.

- not being able to narrow a catch to a specific error type.


Another reason why "errors as values" error handling approach is superior.

Compare

    let url = Url.parse(str)?;
To

    let url; // invalid state
    try {
        url = new Url(str)
    } catch (e) {
        return ...
    }


I think that this is sort of cheating. You could have a language that uses "?" as sugar for the more wordy try-catch-return code that you've written here. These examples aren't really showing the semantic difference between errors as values and thrown errors, which is the implicit propagation across multiple code locations.


I love cheating!

> You could have a language that uses "?" as sugar for the more wordy try-catch-return code that you've written here.

I do! (it's ">>=" though, not "?").

> These examples aren't really showing the semantic difference between errors as values and thrown errors, which is the implicit propagation across multiple code locations.

Well here's where we can start to go in circles. I like errors as values, and I dislike Checked Exceptions. But I've seen others argue that Result and Checked Exceptions are completely equivalent. Different people have different ideas about what the semantics are.


True, this might not be the best example, but it shows how much more control and flexibity former approach provides, given rich support fo Result type.


Where is your error handing logic in the top example? At the very least you’d need an if statement. This is comparing apples to oranges.


It's using rust's ? operator. This operator evaluates to the value if the Result it's called represents success, and propagates the error if it represents failure. This assumes the calling function also returns a Result with an error that's convertible from the error the called function operated on.

In many cases that's enough to handle an error. It's still not a great comparison, since implicitly letting the exception propagate is the closest correspondence in a language that uses exceptions for expected errors.


> implicitly letting the exception propagate

It’s not really implicit though, is it? You’re explicitly asking the language to unwrap the result or propagate the error up out of the function. And the function return value explicitly encodes this, unlike exceptions which can happen anywhere for any reason at any time.


Great! Now there are three different ways to call this API! Just because one dude has a different preferred coding style...

Now, arguably the original API wasn’t the best, but this is really not the way to design an API. There should preferably only be one way to do things and consistency with the rest of the library should be kept. Finally, don’t forget about browser compatibility.

As many others already argued, the root cause can be solved with just a sprinkle of syntax sugar to aid with try/catch.


Recently ran into this where we wanted to avoid new URL for the memory overhead, and was blown away that JavaScript's URL does not contain a static parse method. Things like this are why people working with real languages make fun of us.


Wouldn't a static parse method still allocate memory for the object that it returns?


> Wouldn't a static parse method still allocate memory for the object that it returns?

Yes, but you don't have the overhead of constructing URL first. The new URL.canParse method from the article covers this.


[flagged]


An Exception should be exactly that: an Exception, not an expected control flow for normal situations. Yes, I get that these are not all that different in practice, but then again, `throw` isn't all that different than `goto` either, and `catch(err)` definitely implies something other than "if(url)", and covers a much more ambiguous set of scenarios on its own.

Okay so I guess I'm also a guy who can't handle try catching


Clearly he can handle it given that he mentioned try-catch and the cons of using that approach (cant use const, the control flow is exception-driven, increased verbosity, etc.)

The key takeaway of the article is that the author kickstarted interest in extending the URL API so that people can actually validate URLs. Not many people are fond of the idea of "validation by running a function and seeing if it throws an exception".


> Not many people are fond of the idea of "validation by running a function and seeing if it throws an exception".

That's only because JS makes it unnecessarily verbose in the case where they don't care about the error message. It's exactly what they should want to do, rather than add another piece of the API to have to remember.


It's a nice tip (the existence of URL.parse) but doesn't scream "get shit done" like `import { parseUrl } from 'lib/url'` (or parseJson, etc)


Article's about a guy who doesn't like using try catch as control flow


Apparently he finds null checking better


"Exception" has a meaning. Exceptions are supposed to be used for just that, unexpected situations. Not being able to parse something is not an exception. It's a normal thing. RegEx doesn't throw an exception when there's no match. Array.indexOf doesn't throw an exception when it doesn't find a something.

It's really nice to able to go into the debugger and say "stop on all exceptions" and not be spammed with the wrong use of exceptions

And so, it's nice that we'll have `URL.parse`


If you're expecting a string to be a URL, failing to parse it as a URL is indeed an unexpected situation.


An invalid URL in a config file is exceptional. An invalid URL typed in by a user or from an external source (eg the body of an API request or inside of some HTML) is Tuesday.


You don't?

If that's the case, can you explain why?


Null checking can be fine if a failure mode is unambiguous. However, if an operation can fail for many reasons, it can be helpful to carry that information around in an object. For example with URL parsing, it might be nice to be able to know why parsing failed. Was it the lack of protocol? Bad path format? Bad domain format? Bad query format? Bad anchor format? This information could theoretically be passed back using an exception object, but this information is eliminated if null is returned.


Exceptions and null both require the caller to 'deal with it' verbosely, so neither saves lines of code.

But the only thing more annoying than a loud failure (exceptions) is a silent failure (null) which keeps executing.




Consider applying for YC's Spring batch! Applications are open till Feb 11.

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

Search: