12 ms·
The problem with new URL(), and how URL.parse() fixes that
- rixary 2y ago[flagged]
- strombofulous 2y agoArticle's about a guy who doesn't like using try catch as control flow
- InfiniteRand 2y agoApparently he finds null checking better
- nox101 2y ago"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`
- valicord 2y agoIf you're expecting a string to be a URL, failing to parse it as a URL is indeed an unexpected situation.
- maxbond 2y agoAn 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.
- justin_oaks 2y agoYou don't? If that's the case, can you explain why?
- wewtyflakes 2y agoNull 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.
- mrkeen 2y agoExceptions 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.
- IggleSniggle 2y agoAn 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
- koito17 2y agoClearly 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".
- happytoexplain 2y ago> 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.
- grrowl 2y agoIt'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)
- minebreaker 2y agoWhy can't we just use `Either` or ADT
- wk_end 2y agoWith TypeScript a nullable return type effectively is an ADT.
- seniorsassycat 2y agoBut it's not Either / Result because there no error information on null
- tengbretson 2y agoThen return something else.
- maxbond 2y agoYou might be interested in `fp-ts` (and the related `io-ts`). https://gcanti.github.io/fp-ts/ https://gcanti.github.io/fp-ts/ https://gcanti.github.io/io-ts/ https://gcanti.github.io/io-ts/
- adregan 2y agoI’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?
- maxbond 2y agoNo 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.
- happytoexplain 2y agoSwift, 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.
- nox101 2y agoThat's arguably a poor use of exceptions. Swift has URL.init which returns nil if parsing fails. It doesn't throw.
- happytoexplain 2y agoI 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.
- taspeotis 2y agoIn .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.
- throwaway2037 2y ago> means creating an exception which costs you an allocation Please. What percent of .NET users are really worried about that allocation? Maybe 0.01%?
- deleted 2y ago[deleted]
- Waterluvian 2y agoI 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.
- aabhay 2y agoExactly! 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.
- ramesh31 2y agoRecently 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.
- unchar1 2y agoWouldn't a static parse method still allocate memory for the object that it returns?
- ramesh31 2y ago> 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.
- technion 2y agoIt'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:// 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.
- cmaggiulli 2y agoAre 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.
- afavour 2y agoI 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.
- Izkata 2y agoThere'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.
- dkarras 2y agojust 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.
- mjevans 2y agoSomething like... if (typeof URL.parse != 'function') {URL.parse = function (_s){let _ret; try { _ret = new URL(_s)} catch {}; return _ret} } ;
- RaisingSpear 2y agoYeah, 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/ 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.
- hyperhopper 2y agoIronic 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.
- RaisingSpear 2y ago> 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.
- croes 2y agoEspecially because we have npm packages for things like left-pad. So this is a no-brainer.
- jeroenhd 2y ago
- qbane 2y agoI 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.
- jakub_g 2y agoRe: 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=readme-ov-file#use-case-3-try-catch-initialized-bindings https://github.com/nzakas/proposal-write-once-const?tab=read...
- qbane 2y agoThanks for the reference. I am surprised that Java already has a solution and the choice of keyword "final" is a nice one.
- throwitaway1123 2y agoI 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 https://github.com/tc39/proposal-do-expressions
- IggleSniggle 2y agoThanks for this, I think your syntax is an improvement over the solutions I've tried
- ngruhn 2y agoI 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.
- jeroenhd 2y agoIt'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:// https:// and http:// http://, or make use of the weird double slash HTTP introduced into the URL by matching on :// instead.
- mstade 2y agoWell, 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!
- Joker_vD 2y agoWhy 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.
- cageface 2y agoIt 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.
- ivanjermakov 2y agoAnother 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 ... }
- Aeolun 2y agoWhere 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.
- CodesInChaos 2y agoIt'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.
- stouset 2y ago> 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.
- UncleMeat 2y agoI 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.
- robertlagrant 2y agoThe 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.
- thomasreggi 2y agoI 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.
- Too 2y agoGreat! 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.