Some Node pitfalls – 1. Global state

I’ve been teaching myself Node over the last few weeks, by building a non-trivial application. In the process, I’ve been faced with a number of difficulties that I think are worth blogging about. First, let me say that I really like Node. It’s fast, powerful, smartly-designed, and has a liberating effect on my programming style. It is definitely trying to steer developers to a pit of success. It tends to do so, however, by surrounding the pit of failure with deep holes with spikes on the bottom…

Today’s post is about global state in Node. Global state is evil. It is, however, a little too easy to inadvertently create in Node. Node modules are loaded once, and then cached. That’s a good thing, of course, because reading files is expensive, and ‘require’ is a synchronous API. This is no different from a .NET application loading .NET assemblies, except that Node does it more lazily, and with typically smaller units of code (more good things). Because of that, however, anything that is defined on the module’s exports (what the module exposes and that the rest of the application can ‘require’), is essentially static, i.e. global state.

Take this foo.js module for example:

function Foo() {
}

module.exports = {foo: new Foo()};

This code will only be executed the first time the module is ‘require’d. Subsequent ‘require’s will just return the exports object as it was at the end of the first call. This means that require(‘foo’).foo is a singleton. Its state is going to be shared by the whole application.

That may look like an easy one to avoid, but I’ve only showed a trivial case of it. I’m currently investigating a bug in my code where the first request to the server works fine, but subsequent requests crash. I know that there is some hidden global state at play, but I haven’t found it yet. It seems to be hiding really well, and to be a lot more subtle than the above example.

The way I’ve been avoiding global state so far has been to manage my scopes very carefully, and by attaching the state that I need to an object that represents that scope unambiguously. The higher the scope, the more I try to keep state at a minimum, and immutable.

The site knows about the services that are available and enabled, and about its settings, that are immutable (or rather never muted).

Data that is scoped to the request is stored… on the request object, and this is passed around as a parameter when necessary. I’ve tried to keep that to a minimum, but I think I still have too much.

Services are either exposing static, stateless APIs, or they are providing an entirely new instance every time they are needed (they export the constructor, not an instance), and they encapsulate their state.

Did you have similar problems when learning node yourself? If so, how did you solve them?

5 Comments

  • I'm not disagreeing with global state being a problem - a well designed/explicit caching layer is a better option usually. Also I agree in a web app, almost all globals are likely request scoped.

    One place where I've found singletons useful and benefited from node.js modules being loaded only once is to setup the central logging infrastructure for my app as a logging module. Its a simple way to load the logging module anywhere in the app, without starting to rely on passing a logging service everywhere it needs to be accessed.

  • Glad to see you messing with Node! Getting used to Node's caching system can be a pain, but there are simple ways around it. The first thing, as you've done, is to fully understand the caching system. The second is to use it to your advantage.

    If you don't want a cached instance (it's not really a Singleton, nor is it global) the export the prototype and new it up as you need. Or, better yet, use some factories.

    A module is not a place to store state, so I would suggest not doing it. It's an API surface - you can look at the way Express handles this with the app declaration.

  • Thank you both for your comments. Rob, I started on Node with your videos and posts, so thanks! :) Would you mind explaining why you think a cached instance is not a singleton or even global? I understand that strictly speaking, the implementation is not exactly your canonical singleton, but for all practical purposes, they are, aren't they? Were you referring to that? Thanks for the pointer to the Express app object.

  • Sure - first though I think the CAPTCHA below is horrid :). Just sayin'... :)

    Probably mincing words, honestly, but when I think of Singletons I tend to think of "patterns". I *did* say that modules in Node are Singletons - as they basically are - but that's a tad misleading. It suggests that all you can do is generate a single instance of something.

    This is why I suggested exporting the prototype instead of any object. That way you can new up the prototype and avoid any caching issues.

    In terms of "global" - the module isn't accessible unless you require it, which means it's not global. Again - mincing words but when you discuss Node in terms of "Global Singletons" and lead with the word "pitfall" in your title... it suggests something else entirely.

    So - if you want to work with the concept of state I might suggest you have a config object that is instantiated when the app starts. You can do this with a module, if you like, exporting the config or you can use the Constructor pattern (preferred) so you can send in some intializers. Once you have this instance - tack it onto the "app" variable so it's available (truly) globally in your app :).

    Does this make sense?

  • Thanks for taking the time, Rob, makes perfect sense and clarifies a lot. That pretty much confirms what I was thinking. As I'm saying in the post, my services export the constructor, like you suggest. I still need to be careful about the scope and lifetime of my instances, but at least they're not global. I also, do have some global settings that are, precisely, injected into a "shell" object's constructor, and the instance is kept at the global scope. I do not however access it through the app, but always manage to inject it into services (more on that in another post). That enables a few neat tricks, the least of which isn't mocking and testing very easily...

Comments have been disabled for this content.