This article's content
Event Loop in NodeJS

Slow I/O operations such as accessing disk and network resources are handled via Node’s Event Loop. The Event Loop is started once you run a node script and exits once there are no more callbacks to process.

Heap and Stack

V8 is node’s runtime engine and mainly consists of a Heap and a Stack. The Heap is memory that V8 allocated to store objects, for example if we invoke a function then an area in the heap is allocated to act as a local scope of that function.

A Stack is a First In Last Out data structure. Every time a function is invoked it gets pushed to the stack and every time it returns it gets popped off the stack. A function together with its arguments is called stack frame.

Event Loop and Queue

Node is providing timers, events and wrappers to interact with filesystem and networks. Node also provides the Event Loop and Queue (also referred to as Message Queue or Callback Queue) using the libuv library.

A queue is a First In First Out data structure. You add functions to Node’s queue via specific methods that Node provides, such as Timer functions. For example when the call stack sees setTimeout(cb, 1000) then setTimeout invokes a Node timer function outside of the V8 JavaScript runtime, pops setTimeout off the stack and let other code continue to run. Then after (at least) 1000 milliseconds the Timer adds cb to the Node queue. The Event Loop processes the queue by calling its functions. In the example the queue invokes cb, or in other words, adds cb to the stack.

Job of the event loop: If call stack is NOT empty, then wait. But if it is empty and there is an event waiting in the queue, then execute the queued function which will put it on the stack. If the stack and queue are both empty, Node will exit the process.

Slow code on the stack will block the Event Loop.

Phases of Event Loop

The event loop itself has different sequential phases, for example timers run in one phase, I/O callbacks in another. setImmediate(cb) is a Node timer function that runs in a separate phase, that’s why setImmediate(cb) is executed before setTimeout(cb, 0). Then there is also the misleading process.nextTick(ntCb) function, which actually does NOT execute on the next tick, because the function is not part of the event loop and does not care about the different phases of the event loop. ntCb is processed after the current operation completes and before the event loop continues, which is both useful and dangerous.

Creating custom events with Event Emitter

const EventEmitter = require('events');
const ee = new EventEmitter();

ee.on('greet', (evt) => {
    console.log('Event is: ' + evt);
});

ee.emit('greet', 'hello');

results in Event is: hello.

A more elaborate example

In the following example we

  • read the current script file and use event ‘data’ to display its file length
  • read a non-existent file and handle the error event
  • measure execution time of the asynchronously invoked file-reading function
  • use class inheritance
  • use ES6 modules with import instead of require.
import {EventEmitter} from 'events';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { dirname } from 'path';

/* Note that __filename and __dirname are not available in ES6 modules, that’s
   why we have to create them manually. */
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

class RunWithLog extends EventEmitter {
    run(asyncFunc, ...args) {
        console.time('measure');
        this.emit('start');
        asyncFunc(...args, (err, data) => {
            if(err) {
                return this.emit('error', err);
            }

            this.emit('data', data);
            console.timeEnd('measure');
            this.emit('end');
        });

    }
}

const runWithLog = new RunWithLog();

/* We listen to start event */
runWithLog.on('start', () => console.log('Starting'));

/* We listen to start event again and usually implementation order counts, but here
   we use prependListener to have this event be handled earlier  */
runWithLog.prependListener('start', () => console.log('Before start'));

/* We listen but remove listener for event 'end' just for illustration */
const onEnd = function() {
    console.log('Ending')
};
runWithLog.on('end', onEnd);
runWithLog.removeListener('end', onEnd);

runWithLog.on('error', (err) => console.log('Error', err));

runWithLog.on('data', (result) => console.log('Data length', result.length));

runWithLog.run(fs.readFile, __filename);

Results in (roughly):

Before start
Starting
Data length 1384
measure: 6.201ms

About Author

Mathias Bothe To my job profile

I am Mathias, born 40 years ago in Heidelberg, Germany. Today I am living in Munich and Stockholm. I am a passionate IT freelancer with more than 16 years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I am founder of bosy.com, creator of the security service platform BosyProtect© and initiator of several other software projects.