Search code examples
javascriptnode.jsenvironmenthoisting

Why am I getting "Cannot access 'server' before initialization" error in NodeJS?


I am getting the dreaded Cannot access 'server' before initialization error in code that is identical to code that's running in production.

The only things that have changed are my OS version (macOS 10.11->10.14) my NodeJS version (10->12) and my VSCode launch.json, but I cannot see anything in either that would cause an issue. My Node version went from 10 to 12, but in production it went from 8 to 15 without issue. I routinely keep launch.json pretty sparse, and the same error happens using node server in Terminal.

Here is the offending code. The issue occurs because I have shutdown() defined before server and it references server. It's written to add an event-handler and then cause the event. Yes, it could be refactored but it already works. It works, really. In 21 instances spread over 7 servers.

I have tried changing the declaraion/init of server from const to var but that does not fix it. As mentioned, this is code that's running in prod! What's wrong with my environment?

Maybe a better question is: why did this ever work?

'use strict'

const fs = require('fs');
const https = require('https');
const cyp = require('crypto').constants;
const stoppable = require('./common/stoppable.js');
const hu = require('./common/hostutil');

process.on('uncaughtException', err => {
  wslog.error(`Uncaught Exception: ${err} ${err.stack}`);
  shutdown();
});

process.on('unhandledRejection', (reason, p) => {
  wslog.error(`Unhandled Promise Rejection: ${reason} - ${p}`);
});

// 'shutdown' is a known static string sent from node-windows wrapper.js if the service is stopped
process.on('message', m => {
  if (m == 'shutdown') {
    wslog.info(`${wsconfig.appName} has received shutdown message`);
    shutdown();
  }
});

process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
process.on('SIGHUP', shutdown);

function shutdown() {
  httpStatus = 503; // Unavailable
  wslog.info(`${wsconfig.appName} httpStatus now ${httpStatus} - stopping server...`);
// Error happens on this next line; It should not execute till after server is running already
  server.on('close', function () {
    wslog.info(`${wsconfig.appName} HTTP server has stopped, now exiting process.`);
    process.exit(0)
  });
  server.stop();
}

// Init and start the web server/listener
var combiCertFile = fs.readFileSync(wsconfig.keyFile, 'utf8');
var certAuthorityFile = fs.readFileSync(wsconfig.caFile, 'utf8');

var serverOptions = {
  key: combiCertFile,
  cert: combiCertFile,
  ca: certAuthorityFile,
  passphrase: wsconfig.certPass,
  secureOptions: cyp.SSL_OP_NO_TLSv1 | cyp.SSL_OP_NO_TLSv1_1
};

var server = https.createServer(serverOptions, global.app)
  .listen(wsconfig.port, function () {
    wslog.info(`listening on port ${wsconfig.port}.`);
  });

server.on('clientError', (err, socket) => {
  if (err.code === 'ECONNRESET' || !socket.writable) { return; }
  // ECONNRESET was already logged in socket.on.error. Here, we log others.
  wslog.warn(`Client error: ${err} ${err.stack}`);
  socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});

server.on('error', (err)=>{
  if ( err.code === 'EADDRINUSE' ) {
    wslog.error(`${err.code} FATAL - Another ${wsconfig.appName} or app is using my port! ${wsconfig.port}`);
  } else {
    wslog.error(`${err.code} FATAL - Server error: ${err.stack}`);
  }
  shutdown();
})

combiCertFile = null;
certAuthorityFile = null;

// Post-instantiation configuration required (may differ between apps: need an indirect way to plug in app-specific behavior)
stoppable(server, wsconfig.stopTimeout);

// Load all RESTful endpoints
const routes = require('./routes/');

Solution

  • This is a runtime error, which happens only in a very specific situation. But actually this exact error shouldn't happen with var server = ... but only with const server = ... or let server = .... With var server = ... the error message should say "Cannot read properties of undefined"

    What happens

    You have an error handler for uncaughtException which is calling shutdown() and in shutdown() you are referencing your server. But consider what happens if your code throws an exception before you initialized your server. For instance if your cert or key cannot be read from the disk, cert or key are invalid ... So nothing will be assigned to server, and an exception will be raised.

    Then the handler for your uncaught exception will fire and call the shutdown() function, which then tries to access the server, which of course hasn't been initialized yet.

    How to fix

    Check what the unhandled exception is, that is thrown before your server is initialized and fix it. In your production environment, there is probably no exception, because the configuration and environment is properly set up. But there is at least one issue in your develepment environment, which causes an exception.

    Difference between var and const

    And the difference between var server = ... and const server = ... is quite a subtle one. For both, the declaration of the variable is hoisted up to the top of their respective scope. In your case it's always global, also for const. But variables declared as var are assigned a value of undefined whereas variables declared as let/const are not initialized at all.

    You can easily reproduce this error if you uncomment either error1 or error2 in the following code. But error3 alone won't produce this ReferenceError because bar will already be initialized. You can also replace const bar = with var bar = and you will see, that you get a different error message.

    process.on("uncaughtException", err => {
      console.log("uncaught exception");
      console.log(err);
      foo();
    });
    
    
    function foo() {
      console.log("foo");
      console.log(bar.name);
    }
    
    function init() {
      // throw new Error("error1");
      return { name: "foobar"}
    }
    
    // throw new Error("error2");
    const bar = init();
    
    //throw new Error("error3");