Abuse of the nullish coalescing operator in JS/TS
Posted by fredrikaugust@reddit | programming | View on Reddit | 50 comments
Posted by fredrikaugust@reddit | programming | View on Reddit | 50 comments
Ginden@reddit
throw expressions proposal solves this.
right_makes_might@reddit
Why would you do this? This has an identical effect as just accessing the field with
.It only changes the wording of the error thrown in the case that user is undefined.
fredrikaugust@reddit (OP)
At least one benefit I can see, without having read the proposal, is that you make it clear at the call site that an error might occur there, and you also indicate to the TypeScript type checker that the nullish-ness is dealt with. The other, as you mentioned, would be to make "cannot read properties of undefined (reading 'x')" into something easier to investigate once it pops up in your monitoring tools.
spaceneenja@reddit
I think what’s confusing me is the example is incomplete. We still need to catch and handle the error gracefully, with empty string being notoriously better than a crash.
theScottyJam@reddit
Only if undefined is actually expected. If you don't believe undefined to be expected, then the throw should be unreachable, and there's nothing to catch and handle.
(And if a bug is later introduced and the throw becomes reachable, then in many cases a full crash is preferred then an attempt to keep running with a nonsense value).
Mesqo@reddit
Imagine you're creating a frontend with a large and unstable backend api, most of which is legacy. And because an app is a public service and not some internal software, presenting a user with a crash is a bad idea overall. Hell, even telling the user the fact that some error occurred may negatively affect the conversion rates by frightening potential clients. It's better to just use logs for errors but not present user with every misshapening unless it explicitly prevents the usage of a site.
theScottyJam@reddit
There's a teer.
A full crash is still prefered over a nonsense value. And providing well-coded fallback behavior with good logging (as you're suggesting) is preferred over a full crash. Sometimes there's not a practical way to provide fallback behavior though, or sometimes you just don't have the resources to implement all of the fallback behavior at the moment, in which case, a full crash is the next best option.
lalaym_2309@reddit
Don’t crash the user, but also don’t hide failures; throw at domain boundaries, catch at the UI boundary, log hard, and swap to a safe fallback. Use an app‑level error boundary (React/Vue) per route to show a friendly “temporarily unavailable” state, not a white screen. Wrap flaky endpoints with timeouts and a circuit breaker; serve cached/partial data and disable actions that require fresh data. Validate responses (zod/io‑ts) at the edge; if a must‑have field is missing, trip the fallback and log, don’t render nonsense. Batch logs from the client with sampling and a correlation id; add a remote kill switch (LaunchDarkly/ConfigCat) to turn off a broken feature fast. I’ve used Sentry for crash capture and New Relic for traces, with DreamFactory as a quick REST intake to store client logs when the core API was unreliable. Bottom line: fail safely, log loudly, keep the UI usable
spaceneenja@reddit
This is all I am saying lol. Saying you should not expect an error is famous last words. You can log the error and provide a fallback.
Mesqo@reddit
This "temporarily unavailable" state is the last resort - it's shown only the app or a feature is completely unusable. For most other cases it's just fine to move on. It's financially better to later for support stuff to solve client's problem than to not have that client at all ;-)
And yeah, most of what you listed is present, just with different names. But I like the idea of a "kill switch" - will use it to make tasks and parameters =)
Heavy_Magician_2649@reddit
Wanted to say something similar, if you have a bunch of legacy data that has been migrated to and from different places in your databases millions of times, some data might have different key/value names or even locations based on recency. Others have mentioned very viable workarounds for this, but in general I feel like the nullish coalescing is like any other piece of syntax sugar: if you know what you’re doing with it, it helps tremendously; if you don’t know what you’re doing with it, it will become a crutch.
Mesqo@reddit
BE can be very inconsistent, and while BE does the heavy lifting it's FE that the user see. And if BE shits badly it's up to FE to present that to user with as much perfume as possible =)
barthykoeln@reddit
Instead of throwing a default
ReferenceErrororSyntaxErroryou can throw a custom Error, that can be picked up and acted upon by other parts of the application, an error dashboard, console logs, the UI, etc etc.vytah@reddit
But you're not accessing any field.
.throws if the left side is undefined, but here we're checkingname, which is on the right.Absolute_Enema@reddit
This is not the case in JS though, is it?
R2_SWE2@reddit
Slightly different behavior right? This version can throw if user is an object but name is nullish whereas direct property access would not throw
yksvaan@reddit
Just assign a reasonable default value first, then change conditionally.
enderfx@reddit
But the default value is what the ?? Is trying to achieve, and what the writer is against
yksvaan@reddit
Well if there is no possible default value then obviously you need to react to it, raise an error or whatever.
But I mean instead of these ternaries make it more explicit, first assign null, 0, -1or whatever and then do the checks.
You could basically do like:
function getFoo() { let foo =null if ( bar) { foo=123 }
return foo }
Then you just null check when calling, use wrapped errors, error return values or whatever. The point is that it's much more robust than overusing ternaries.
Reashu@reddit
What? This is worse in almost every way.
yksvaan@reddit
How so? Firstly you have a valid closed result set, e.g. a string containing API address or null if it's invalid. Use the worst case as default value and define it immediately.
Keep simple control flow and guard clauses to return early if needed. That's basically 1:1 how the logic goes and very easy to read.
Calling function doesn't need to know any details, just null/err check as usual. How you want to encode the error case, that's another discussion. Personally I think null is perfectly fine.
Reashu@reddit
But we are talking about how to handle null at the call site...
Either way, I don't like your code because there's unnecessary mutation.
yksvaan@reddit
Dont' see how a mutation changes anything, in both cases runtime is storing function return value and possibly conditionally changing it later. In fact it's basically identical (byte)code
Reashu@reddit
If a variable is mutable I have to look out for any changes between declaration and whatever line I'm actually interested in. If it's constant, I only have to find the initialization (and IDEs can provide that on hover).
enderfx@reddit
Yep, but I think that was what the author was saying: ensure presence of the data at a higher layer, and not (need to) use the coalescing ?? operator. As in, not having that default/fallback “” or “-“ because you never want to render that.
I take this with a pinch of salt. As usual, it is all about knowing the tool and when to use it. You could want to write a function that applies some properties to an object, using a default one if something is undefined because of. Then ?? Is perfectly fine.
yksvaan@reddit
Yeah that ?? "" is basically used to make it a string to suppress linter/ts warnings. And pretend everything is good. It's kinda lazy way and expecting the happy path is the case.
Nothing wrong with using "" as default value though, it's just about use case and how you handle it. In some languages ( e.g. go) "" is the default value for string so you'd use a pointer to express lack of value vs empty value.
enderfx@reddit
Exactly! yet another case of "just know what to use your tools for"
tracernz@reddit
I didn’t take it that way. It’s more that they’re against when people just reflexively reach for
?? ‘’to make the red squiggles go away rather than stopping to think how and where the undefined or null state should actually be handled. A sane nullish coalesce where it makes sense is fine.enderfx@reddit
Indeed. And agree completely. I was just arguing that - from what I got - in this context and related to this post, the “reasonable default” would not make sense. I.e.: there’s no no reasonable default for a user name label if there is no user name - just don’t render it.
fredrikaugust@reddit (OP)
This was what I tried to write in the post as well. Agree with everything you said here! I think
??is very handy for providing a default value, and I don't see anything bad in that.Jaded-Asparagus-2260@reddit
What is a reasonable default value for a name, yksvann?
yksvaan@reddit
It depends. Could be null, "" could work if it's e.g. appended and trimmed to a single field. Maybe you have migrated data and have to use "" instead of missing value for some reason...
The point is, the dev writing the code knows what it should be, what are possible values etc. There's no absolute rule for anything.
Jaded-Asparagus-2260@reddit
There is no reasonable default for names. Nobody is named "". Even thinking about a default is the wrong idea here. You don't need defaults, you need proper handling of missing/undefined data.
Ronin-s_Spirit@reddit
This is a skill issue, can happen in any language.
Mesqo@reddit
Because it's a problem with meaningless default values, not with null coalesce operators.
strange_username58@reddit
I prefer ||
Mognakor@reddit
And then you get hit by falsy values like 0
strange_username58@reddit
Never really have that problem considering they are using it for strings.
fredrikaugust@reddit (OP)
I have to agree with @Mognakor here. I don't see how
||would be any better, and would argue that for this use-case of providing a fallback value to nullable values, it is worse. In the case ofstring | undefined("") ornumber | undefined(0), both of those can trigger the fallback, whereas with??only one will. I'm not really sure I understand what you mean by me wanting""to be falsy in the article.claymir@reddit
This can happen with shitty backend interfaces
Better-Avocado-8818@reddit
Abuse? What a weird way to describe the idea of not writing code that does things you don’t want your program to do.
fredrikaugust@reddit (OP)
Not sure I'm following, but the thought behind the title was describing (if a bit exaggerated — I will admit) the heavy use of
?? ""as abuse of the operator. The use, as I see it, is to provide a sensible fallback value for a nullable value, whereas using?? ""as a shortcut to get rid of the nullability is _ab_use. That's also why I compared it to.unwrapin Rust, which I think people see as more of a problem, even though, in my experience, they are often used for the same purpose.Better-Avocado-8818@reddit
I admit I only quickly read through the article. But it seemed like the main problem pointed out is having an empty string as a default without considering if that default value is actually acceptable. So the core problem doesn’t really seem to be about a specific syntax and more one of type safety or acceptable UI state. Nullish coalescing operator is being used correctly in the example, as in it does exactly what we’d expect, it’s just that the code written isn’t valid behavior for the application.
So I think I actually agree with your article but think the title isn’t representing the more important point you’re making about putting thought into default values, validating unknown data or handling possibly undefined values properly.
fredrikaugust@reddit (OP)
Aha, I see what you mean. I agree. It's perhaps not the best title as, as you point out, the main issue is invalid default values. The reason I wrote about this one specifically is because I see it very often, and thus have come to associate this pattern with that operator, even though it obviously also has a lot of "legitimate" uses. I think a good reason for that is because, unlike
ifand other "tools" in JS/TS,?? ""can be used when passing in props in various frontend frameworks, and is very easy to write, thus is has become a commonly used snippet to avoid nullability. I've been writing a lot of Svelte and React the last few years, so perhaps I've been coloured by that:) Thanks for taking the time to read the article and sharing your opinions:)Better-Avocado-8818@reddit
I admit I only quickly read through the article. But it seemed like the main problem pointed out is having an empty string as a default without considering if that default value is actually acceptable. So the core problem doesn’t really seem to be about a specific syntax and more one of type safety or acceptable UI state. Nullish coalescing operator is being used correctly in the example, as in it does exactly what we’d expect, it’s just that the code written isn’t valid behavior for the application.
So I think I actually agree with your article but think the title isn’t representing the more important point you’re making about putting thought into default values, validating unknown data or handling possibly undefined values properly.
Sad-Tomorrow9789@reddit
Read your article, seems to be a bit simplist.
+ I simply can not easily fail. Don't get me wrong, i'd love to. Failing ASAP is one of the best ways to catch bugs before they go live, or even limit old bugs in your code. But depending on your environnement, you sometimes can not.
+ The data i work with is old, sometimes has gone through a few dozen migrations over the years, a few of which have left a bunch of stupid edge cases. And it doesn't belong to me. Using better data validation ? Yeah sounds good, but now we have to call an old customer that it needs to stop sending that stupid broken payload. The program was developped 10 years ago by an underpaid team of graduates and he doesn't have the knowhow to update it ? Welcome to Corporate Software Programming !
So yeah, you end up writing shit like
```
const description = item?.description ?? '';
return doSomeStuffWithThat(description);
```
Does it work ? yeah. Is it unmaintable ? I mean, the whole project already is and we won't have the budget to attempt to do it properly until 2027. In the grand scheme of things, it's not too bad.
Now, failing early is good advice and should be your goto solution when you still can that is.
captbaritone@reddit
I wrote a similar article in 2022: https://jordaneldredge.com/defaulting-to-empty-string-is-a-code-smell/
Absolutely worth linting against.
fredrikaugust@reddit (OP)
Great article! I definitely agree with the point about letting the caller pass in a nullable value if that indeed is a possible value:)
Reashu@reddit
The comparison with Rust's
unwrapis a bit halting given that we haveunwrap_or_(else). But yes, default / fallback values should be "valid". Obviously.fredrikaugust@reddit (OP)
That's true. I guess what I was aiming for was comparing the explicit case of
|| ""withunwrapas a way to get rid of theundefinedpart of the type union, butunwrap_oris definitely the Rust equivalent.