The problem with new URL(), and how URL.parse() fixes that
kilianvalkhof.com
kilianvalkhof.com
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.
A very similar thing, the Result type in Rust, does pretty well. (Much like Maybe / Either do in Haskell.)
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.
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.
Incremental changes were pretty fast though. (Then IDEA came.)
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.
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.
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".
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.
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.
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.
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 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.
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.
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.
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.
Nah, i am happy with Java. At least in my domain.
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.
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.
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.
The classic case of this is that the compiler cannot tell that
new StringReader("Example").read()
doesn't throw an SSLException.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.
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.)
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.
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.
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.
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.
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.
> They aren’t. Look into languages with algebraic effects.
Can you give me more to go on than that?
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.
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.
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.
`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.
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.
> means creating an exception which costs you an allocation
Please. What percent of .NET users are really worried about that allocation? Maybe 0.01%?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.
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.
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.
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)
Saying "I prefer Maybe to try/catch" is too broad - it's almost like saying you prefer not to handle errors.
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?
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-...
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.
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).
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.
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.
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.
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.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.
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?
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.
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).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.
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.
(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.)
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.)
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.
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.
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.
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.
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.
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.
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.
But it not fun when writing systems. That is why we have different tools for different jobs.
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.
"... 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.
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 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.
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.
https://github.com/nzakas/proposal-write-once-const?tab=read...
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-expressionsAs 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)
if (typeof URL.parse != 'function') {URL.parse = function (_s){let _ret; try { _ret = new URL(_s)} catch {}; return _ret} } ;
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.
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.
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.
So this is a no-brainer.
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.
Can you explain how helpers create exploits? Are there any examples?
It's easier to secure a JIT with 100 methods than it is to secure a JIT with a thousand.
I understand people might not like exceptions, but special-casing certain calls like just makes the language harder to use.
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.
> 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.
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. www.example.com/static/index.html + ./images/foo.jpgI think you just misunderstand what a URL is.
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?
Isn't that what document.baseURI is for? At least that's my understanding:
new URL("/whatever", document.baseURI)
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.
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!
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?
Mostly responding so people with better context don't wait on me.
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.
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.
Compare
let url = Url.parse(str)?;
To let url; // invalid state
try {
url = new Url(str)
} catch (e) {
return ...
}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.
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.
> 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.
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.
Yes, but you don't have the overhead of constructing URL first. The new URL.canParse method from the article covers this.