Global variables are not the problem
55 comments
·January 31, 2025Jtsummers
Izkata
> it is the wrong place to put the count information.
I'd argue this is the case regardless of lifetime. It's trying to squash two unrelated things into one object and should have been two different arguments.
Way more obvious if "obj" is replaced with some example object instead of an empty one:
let person = { name: "Foo Bar", age: 30, counter: counter };
roenxi
I like the diagnosis.
My JS is terrible, but it seems like once you make the counter a global variable it is just better to change it to have an atomic dedicated count function. So instead of incrementing the counter in simple, a globalCount() function gets called that isolates the state. Something like
{
let i = 0;
var counter = function (){
console.log(++i);
}
}
Then call counter() to count & log and document that something horrible and stateful is happening. I wouldn't call that a global variable although the article author disagrees.levodelellis
Could you tell me where this was posted? I thought no one would see this after I got no comments the first day
No one I showed this to complained about the first example but online many people did. I wrote a completely different article which I think is much better that uses examples I would have used in the follow up. I'll post that article next week
Jtsummers
Second chance pool. This post is, per your submission history, from 2 days ago. HN has a second chance pool that lets articles continue collecting upvotes (more easily than digging through the full history). Some of those articles will get their timestamp updated to artificially inflate their ranking. This brings them to the front page again and gives them their "second chance". After a few hours or whatever time, the timestamp is reverted and they'll start falling back into their natural place in the rankings.
https://news.ycombinator.com/submitted?id=levodelellis
null
SpaceNoodled
The counter should be declared as static inside the function, thus limiting is scope and avoiding pollution of the global namespace.
Jtsummers
In this case, yes. Its scope should be the lowest necessary scope. Does JS provide static variables in functions? If not, then that forces you to lift it to some other scope like file or module scope or the surrounding function or class if that's viable.
null
billforsternz
From the article> "Static Function Variable: In C inside a function, you can declare a static variable. You could consider this as a local variable but I don't since it's on the heap, and can be returned and modified outside of the functions. I absolutely avoid this except as a counter whose address I never return."
These variables are not on the heap. They are statically allocated. Their visibility is the only thing that differentiates them from global variables and static variables defined outside of functions.
I think such variables can be useful, if you need a simple way of keeping some persistent state within a function. Of course it's more state you are carrying around, so it's hard to defend in a code review as best practice.
Amusingly, you can modify such variables from outside the function, simply by getting your function to provide the modifying code with a pointer to the variable, eg by returning the variable's address. If you do that though you're probably creating a monster. In contrast I think returning the value of the static variable (which the author casts shade on in the quote above) seems absolutely fine.
Edit: I should have stated that the number one problem with this technique is that it's absolutely not thread safe for obvious reasons.
hansvm
Global variables (in languages where they otherwise make sense and don't have footguns at initialization and whatnot) have two main problems:
1. They work against local reasoning as you analyze the code
2. The semantic lifetime for a bundle of data is rarely actually the lifetime of the program
The second of those is easy to guard against. Just give the bundle of data a name associated with its desired lifetime. If you really only need one of those lifetimes then globally allocate one of them (in most languages this is as cheap as independently handling a bunch of globals, baked into the binary in a low-level language). If necessary, give it a `reset()` method.
The first is a more interesting problem. Even if you bundle data into some sort of `HTTPRequest` lifetime or whatever, the fact that it's bundled still works against local reasoning as you try to use your various counters and loggers and what have you. It's the same battle between implicit and explicit parameters we've argued about for decades. I don't have any concrete advice, but anecdotally I see more bugs from biggish collections of heterogeneous data types than I do from passing everything around manually (just the subsets people actually need).
serbuvlad
I find the concept of a context structure passed as the first parameter to all your functions with all your "globals" to be very compelling for this sort of stuff.
sublinear
https://en.wikipedia.org/wiki/Dependency_injection
This is very similar to dependency injection. Separating state and construction from function or method implementation makes things a lot easier to test. In my opinion it's also easier to comprehend what the code actually does.
levodelellis
That's exactly why I used this specific example. I seen many code bases that use clone to avoid mutation problems so I wrote this specifically to show it can become a problem too.
I wrote a better article on globals. I plan on posting it next week
bee_rider
That just seems like globals with extra steps. Suddenly if your context structure has a weird value in it, you’ll have to check every function to see who messed it up.
bigcat12345678
That's 2 parts: 1. Global variable (mutable) 2. Local function with context argument (mutations)
You have clear tracking of when and how functions change the global variable
caspper69
Hard disagree.
If I have 500 functions, I don't want to extrapolate out the overhead of passing a state object around to all of them. That's a waste of effort, and frankly makes me think you want to code using an FP paradigm even in imperative languages.
Module-level and thread-level "globals" are fine. You gain nothing (other than some smug ivory tower sense of superiority) by making your functions pure and passing around a global state object to every single method invocation.
rafaelmn
You get functions that are easily testable in isolation with all state provided in parameters.
You also get explicit dependencies and scoping controlled by caller.
I don't mind globals but saying you get nothing for avoiding them is :/
Gibbon1
I tend to use getter and setter functions to access globals and manage state.
Advantage only the function that depends on the global needs to bring in the dependency.
null
bb88
Please no.
Singletons if you must. At least you can wrap a mutex around access if you're trying to make it thread safe.
levodelellis
You may hate my article next week, it's meant to replace this article. If you want you can email me for early access and tell me how I can improve the article. Lets say you can guess my email if you're emailing the right domain
bknight1983
For every example of a bug caused by not using a global variable I’m sure could find 10 caused by a global variable
grandempire
Why are singletons better? That's just a global object.
> wrap a mutex
What if my program has one thread? Or the threads have clearly defined responsibilities?
dragontamer
Global objects need to be initialized. And if two are ever initialized you run into problems like the above.
Singleton is a pattern to ensure that a global objects is only ever initialized once.
zabzonk
how do you know that? just about every serious bug i have ever written was when i thought i understood multi-threaded code, but didn't.
grandempire
Because as a programmer I have responsibility for the technical soundness of the program, and I don't create threads haphazardly.
> when i thought i understood multi-threaded code, but didn't.
All the more reason to carefully plan and limit shared state among threads. It's hard enough to get right when you know where the problems are and impossible if you spray and pray with mutexes.
rileymat2
You know it is /currently/ accessed on one thread. These are little landmines that add up over time.
grandempire
The burden is on the programmer adding a new thread to know what they can safely access.
The conclusion of your argument looks like 2000s Java - throw a mutex on every property because you never know when it will need to be accessed on a thread.
Designs that spread complexity rather than encapsulate it are rarely a good idea.
Puts
I think the author forgot the most useful use case for globals, and that is variables that has to do with the context the program is running under such as command line arguments and environment variables (properly validated and if needed escaped).
peanut-walrus
Do those change during program runtime though? I don't think many people have problems with global constants.
AdieuToLogic
This assertion made in the article invalidates the premise of same:
With a little encapsulation, you can make globals error-proof ...
levodelellis
What do you suppose is the recommended way to use the encapsulation? ;) This is partly why there's a "defining global variables" section, I know people will will consider some usage as not using a global
grandempire
I've recently found it helpful to think of problems at the scope of an individual process, rather than a set of functions in a library or framework. This makes it much clearer when a global variable makes sense or not, and generally where to initialize things, and place state.
pdimitar
> The problem is data access. Nothing more, nothing less. There's a term for this that has nothing to do with global variables: "action at a distance."
I mean yes, using global variables is just one of the ways to cause action-at-a-distance and that is... apparently a big reveal?
Otherwise sure, there is no pattern that cannot be utilized 100% correctly and without introducing bugs. Theoretically. Now let's look at the practical aspects and how often indiscriminately using such tempting patterns like global variables -- and mutexes-when-we-are-not-sure and I-will-remember-not-to-mutate-through-this-pointer -- lead to bugs down the road.
The answer is: fairly often.
IMO the article would be better titled as "There is no pattern that a lazy or careless programmer cannot use to introduce a bug".
SpicyLemonZest
> The problem is data access. Nothing more, nothing less.
I agree with this, but the problem with global variables is precisely that they make bad data access patterns look easy and natural. Speaking from experience, it’s a lot easier to enforce a “no global variables” rule than explain to a new graduate why you won’t allow them to assign a variable in module X even though it’s OK in module Y.
levodelellis
You might like the article I wrote for next week. Could you tell me where this post is linked from? I didn't think anyone would see this when no one commented the first day
jongjong
Agreed, global variables are fine up to a certain scale, especially if they're only used in the main file. They only really become a problem if you start modifying them from inside different files.
The real underlying problem is 'Spooky action at a distance'; it's not an issue that is specific to global variables. If you pass an instance between components by-reference and its properties get modified by multiple components, it can become difficult to track where the state changes originated and that can create very nasty, difficult-to-reproduce bugs. So this can happen even if your code is fully modularized; the issue is that passing instances by reference means that the properties of that instance behave similarly to global variables as they can be modified by multiple different components/files (without a single component being responsible for it).
That's partly where the motivation for functional programming comes from; it forces pass-by-value all the time to avoid all possibility of mutations. The core value is not unique to FP though; it comes from designing components such that they have a simple interface which requires mostly primitive types as parameters. Passing objects is OK too, so long as these objects only represent structured information and their references aren't being held onto for future transformation.
So for example, you can let components fully encapsulate all the 'instances' which they manage and only give those parent components INFORMATION about what they have to do (without trying to micromanage their child instances); I avoid passing instances or modules to each other as it generally indicates a leaky abstraction.
Sometimes it takes some creativity to find a solution which doesn't require instance-passing but when you find such solution, the benefits are usually significant and lasting. The focus should be on message-passing. Like when logging, the code will be easier to follow if all the errors from all the components bubble up to the main file (e.g. via events, streams, callbacks...) and are logged inside the main file because then any developer debugging the code can find the log inside the main file and then trade it down to its originating component.
Methods should be given information about what to do, they should not be given the tools to do their job... Like if you catch a taxi in real life, you don't bring a jerrycan of petrol and a steering wheel with you to give to the taxi driver. You just provide them with information; the address of your desired destination. You trust that the Taxi driver has all the tools they need to do the job.
If you do really want to pass an instance to another instance to manage, then the single-responsibility principle helps limit the complexity and possibility for spooky action. It should only be passed once to initialize and then the receiving component needs to have full control/responsibility for that child. I try to avoid as much as possible though.
The bug in the program reveals a poor understanding of object lifecycles by whoever wrote it. The `obj` argument to `simple` is not globally unique and so it makes a poor location to store global state information (a count of how often `simple` is called, in this example).
Never tie global state information to ephemeral objects whose lifetime may be smaller than what you want to track. In this case, they want to know how many times `simple` is called across the program's lifetime. Unless you can guarantee the `obj` argument or its `counter` member exists from before the first call to `simple` and through the last call to `simple` and is the only `obj` to ever be passed to `simple`, it is the wrong place to put the count information. And with those guarantees, you may as well remove `obj` as a parameter to both `simple` and `complex` and just treat it as a global.
State information needs to exist in objects or locations that last as long as that state information is relevant, no more, no less. If the information is about the overall program lifecycle, then a global can make sense. If you only need to know how many times `simple` was invoked with a particular `obj` instance, then tie it to the object passed in as the `obj` argument.