Hacker News new | past | comments | ask | show | jobs | submit login

Alternate title for the article: "Lists of lodash features replaced by ES6 if you don't mind throwing an exception when given invalid input such as null or undefined".

All kidding aside, a lot of our lodash code ends up looking something like this:

    function (xs) {
        return _(xs).pluck('foo').filter().value();
    }
That code clearly expects that xs is an array of objects. However, we might occasionally end up with xs being undefined, or with xs being an array but one of the elements is null, etc.

Most of the time, we want our function to just swallow those errors and return an empty array in such cases. This is exactly what lodash does, but if we tried to call xs.map(...) in that case we'd get an error. Similar caveats apply for grabbing the foo attribute if one of the array elements ends up being null or something.

For this reason, I recommend continuing to use lodash almost all of the time, even when there's a native Javascript method available.




> if you don't mind throwing an exception when given invalid input such as null or undefined

That's exactly what I would expect. If only everyone always thrown an exception on any undefined, life would be so much better.


Something I find very interesting about Swift is it's complete reversal of opinion from Objective-C on this topic.

Obj-C basically has the behavior GP wants: call a method on nil (aka: null) and it returns nil/false/0. It's possible to write concise & correct code that relies on this behavior.

Swift turns it into a fatal runtime error, and uses the type system to help prevent it from happening.

I think there's room for both (probably not in the same project though!). A lot of successful projects have been written in Obj-C, and some of them rely on the behavior of nil.

However, it's harder to maintain the code. You have to reason about whether nil is possible and does the code do the right thing, or did the original author forget to consider the possibility? It's really nice when the static type system forces the programmer to be explicit.

Having used both paradigms, I honestly don't know which I'd prefer for JS - especially considering it's lack of static typing. It might depend on whether I was writing application or library code.


There is room for both, but it should be explicit in my opinion. Dart for instance has null safe operators

  // not null-safe:
  foo.bar.baz();

  // null-safe:
  foo?.bar?.baz();
If your type system also does null tracking, then you can see where you might have null values and decide to use the null-safe operators.


Swift has the same syntax for its nullable types. It's also conceptually similar to Haskell's monadic bind over the Maybe type, where your example would look like

    foo >>= bar >>= baz
The nice thing about this approach is that (>>=) is not specific to the Maybe type, and can be extended to other data structures, include one that passes along error messages in the "null" type through the sad path if it encounters them. This would be in the (Either a) monad.


Depending on the particular ___domain of the task.

There are perfectly valid reasons why silently failing is OK.


Seriously. A really common example, ever have a website that completely fails to load when you're running adblock? Would you rather have a blank page, or a 99% functional website with a couple of error messages in the console?


The answer is: that's an expected failure, so you make the error handling explicit by catching the error and taking care of it appropriately (including proceeding to the rest of the page load, naturally). Implicitly handling errors behind JavaScript's loose typing rules is a recipe for disaster.


Explicitly handling errors is almost always a bad idea, no matter what programming language it is. There are very few errors you can reasonably handle, and they must be designed for. It's specific application domains that need different handling strategies: e.g. embedded, device drivers, software that's trying to maintain some very strict invariants. A web page isn't usually one of them.

Usually errors should be sent up the stack to the request / event loop and logged / telemetry / etc.

The question here is something different, though. It's what should be considered an error, vs what should use the null object pattern. I don't think anyone can make a categorical judgement on which is better without context. It's suggested here that the null object pattern implemented by lodash is desired; I don't think it's wrong in principle to rely on it, as part of a cohesive design - e.g. it's used in such a way that it doesn't make development or finding bugs harder than necessary.


Take a look at Common Lisp and Dylan and their respective condition/restart systems. This is how error handling should look like: they let you handle your errors where it makes sense and naturally recover from them if it's possible.


It's hard for me to imagine why would you want to .map() on a list of 3rd party tracking modules but let's go with your example.

That sort of thing should not be handled by a low level utility library, because at best it will only do the right thing 50% of the time. It should be handled explicitly on a single interface between your software and the tracking module.

I would probably wrap it and explicitly ignore things in the wrapping code if the tracking module is missing.

And yes, I would rather see an empty page so I know to temporarily disable the adblocker, than have a page (e.g. seamless) that silently fails to order your food at the very last step.

You will argue that it's better for the users. No it's not. If the site is broken, it's easy for them to understand. They can at least go somewhere else to order food. If everything looks like it's working but it isn't is very-very frustrating.

I can also catch and log errors and fix them, but hidden bugs will just stay longer and frustrate users, because they will think they did something wrong.


> Would you rather have a blank page, or a 99% functional website with a couple of error messages in the console?

It depends if you're running ads on the page... /s


Sure, we can always make up an examples.

Perhaps if you are using null to represent something explicitly in your data. But silently failing on undefined is just going to lead to an other bug somewhere else entirely and half a day of debugging.

I would much rather fail early and loudly than having to hunt down some anecdotal bug that happens every prime-th national holiday and is impossible to reproduce.


I have found it better to have UI code to use undefined over exceptions, and back-end code to use exceptions over undefined as the client should have properly formatted the request.

Having functions/methods return undefined is a huge time and complexity saver for UI code as the application could still be in the process of getting input from the user that then would be passed off to the back-end code once the user was done changing their minds. No point in having a dropdown throw an error because the user is still deciding what they want to appear in the dropdown.


For client-side webapps? Users are just going to hit reload and move on. For just about every website I can imagine, if your two options are to leave an extremely rare bug that's impossible to reproduce, or to effectively take the website down for all users, the former is the unambiguous right choice.


Well, if the bug is so hard to reproduce it would not take the website down.


Catch the exception and pass.

It's why I like python's attitude toward this. It's explicit, not implicit and therefore easier to read and maintain.


Many times folks understand that values can be nullish. Libs like Lodash treat them as empty collections to avoid repetitive nullish check scaffolding.


> we want our function to just swallow those errors and return an empty array in such cases

No no no no. That's a silent failure and a bug. That means our code is doing something we don't intend or understand.


What's wrong with that? For large systems, you'll never have a codebase that is 100% understood or 100% matches what the designers intended. If you want a robust, working, large system, you have to account for unintended things happening some of the time. In many cases, the right thing to do is to preserve or ignore nulls. Especially for client-side JavaScript (where clients are, by their nature, untrusted, and all authentication and data validation must happen on the server whether or not it also happens on the client), if some data fails to load due to a network blip, or the end-user does something unexpected and a div isn't initialized properly, or whatever, the right behavior for the software is to keep going, and the wrong behavior is to cause a minor error to turn into a major one.

In many other cases, of course, the robust thing to do is to catch a failure early and prevent some code from executing before it can do more harm, and err on the side of the system doing nothing instead of it doing something wrong. But neither of these is a universal rule.


This is effectively wishful thinking.

If a bug causes an unexpected undefined value, it will lead your code to an undefined behavior. It might work well 999 times and wipe everything on the 1000th execution. Thankfully, Javascript is mostly limited to web browsers.

Exceptions make it so that nothing unexpected happen. This is especially useful when you do not know the whole codebase. Of course, there are many cases when you can just ignore the errors. Exceptions allow you to fine tune this.


> If a bug causes an unexpected undefined value, it will lead your code to an undefined behavior.

Let's not confuse "undefined", a JS value that is basically like C's NULL, with "undefined behavior", the concept from e.g. the C language spec. Operations on the JS value "undefined" are perfectly well-defined in the C sense; you can reliably test for it and have a case to handle it. In particular, the well-defined behavior for Underscore/Lodash in response to mapping over "undefined" is to return an empty array. The programmer upthread is using the library's documented and well-defined behavior; there is nothing wrong with that.

It is just like how, in C, some functions (like time()) are well-defined if you pass them a NULL pointer, and some functions (like strlen()) are not, and result in undefined behavior. In this case, the functions in question are all well-defined if you pass them "undefined".


> Exceptions make it so that nothing unexpected happen.

No, it makes it so that your program surprises the user by crashing, which is (hopefully!) pretty unexpected.

Unless, of course, you use the exceptions to provide some sane default code path that handles the problem, but that's exactly what lodash is doing for you in this example.


Crashing is exactly what the correct program should do if it hits undefined behavior.


What?

1. Where did anyone say we were hitting undefined behavior? We're hitting a well-defined JavaScript value whose name is "undefined".

2. Why is crashing correct? Don't answer in terms of language specs, answer in terms of desired behavior for whatever you're trying to do with the program. Software engineering is a tool for accomplishing other things. Sometimes, yes, software crashing is the right thing in service of that other goal. But not always.


Undefined behavior of a program is when a programmer didn't define how to handle a particular situation. If an array becomes "undefined" and the programmer didn't expect such result (e.g. there's no if (typeof array === "undefined") { ... }), then the behavior is undefined regardless of what kind of types there are in JavaScript.

Crashing in such case is used to avoid incorrect functioning of the program, such as overwriting user data or introducing security vulnerabilities. By definition, if the behavior wasn't expected, the program is in unknown state. Sure, you can gracefully handle such situations (e.g. allow website to load other scripts if the particular piece that "crashes" not important for its functioning, crash just some kind-of sub-process and restart it, or — if it's user input — end up with some predefined value regardless of the incorrect input, etc.), but the correct _default_ behavior is to crash, otherwise you'll end up with unknown state and the algorithm that you wrote will be incorrect.

I recommend anyone who wants to see how much unexpected behavior is in their JavaScript programs to install typescript@next and start adding types, compiling with tsc --strictNullChecks.


Lodash methods handle things like String#match which returns array/null.

More for avoiding guard scaffolding than for undefined behavior.


> If you want a robust, working, large system, you have to account for unintended things happening some of the time

Ever written a large code base with isolated I/O, functional code, and typed/static analysis? Because I have, and nothing unintended happens except at the I/O level.

When something unintended does happen, it throws an exception: something genuinely exceptional has happened.

This code base has yet to throw an exception in production, and it also hasn't had a bug in production (after running for 6 months with ~1,000 active users).


I'm going to have to dispute your definition of "large system" if it's been running for a mere 6 months and you describe it as if it had a single author. Let me know once it's changed maintenance twice and also once it's changed management twice. Robustness is not about how well a system performs in its initial conditions; it's about how well it responds to change.

Also, from the sounds of it, it doesn't seem like a distributed system. Client-side JS is by its nature a distributed system, dealing with network partitions all the time because end-user internet connections are unreliable.


How could you know without seeing what code he's talking about? Why not take his word?


There's a reason linters/smell-detectors try to catch things like unexpected type coercion, and there's a reason the web software industry is aggressively moving toward typed languages (JS -> TypeScript, for example).

If you tell a computer something it doesn't understand, it should tell you that it doesn't understand. There's no scenario where "just guess what you think I wanted to do and then do it" is a safe or reliable way for a program to run. By definition, sometimes it will work as intended and sometimes it won't. That's a bug. It's the worst kind of bug, actually: silent and undetectable until you catch the problem in the final output.


That's partly an argument for Flow or the eventual strict null flag in TypeScript.

That being said, that was my first reaction too. Null safe code is so important. How you do it (be it with Flow, Lodash, whatever), that doesn't matter, but I do find myself leaning toward libraries over native when payload size doesn't matter too much because of this.

A combination of Flow, Ramda and Sanctuary (for Maybes) if you want to be a bit more niche can give some pretty amazing results.


> That code clearly expects that xs is an array of objects.

Why not call it 'objects' rather than 'xs'?




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

Search: