Node js - Callbacks and Event Emitters

Node.js - Callbacks and Event Emitters

Callbacks and Event Emitters in Node.js

Introduction

Node.js is built around asynchronous, non-blocking I/O operations. Two of the most important concepts enabling this behavior are Callbacks and Event Emitters. These two mechanisms allow developers to handle events and operations that may take time, such as reading files, making HTTP requests, or interacting with databases, without freezing the application.

In this document, we will explore:

  • What are Callbacks?
  • Types of Callbacks (Synchronous and Asynchronous)
  • Error-first Callback Convention
  • Callback Hell and Its Solutions
  • What are Event Emitters?
  • Using the EventEmitter class
  • Custom Events and Listeners
  • Real-world examples of Event Emitters
  • Comparisons, best practices, and much more

What is a Callback in Node.js?

A callback is a function passed as an argument to another function. It is called (or "called back") when an operation completes. This approach is foundational to asynchronous programming in Node.js.

Basic Callback Example


function greet(name, callback) {
    console.log('Hello, ' + name + '!');
    callback();
}

function sayGoodbye() {
    console.log('Goodbye!');
}

greet('Alice', sayGoodbye);

Output:


Hello, Alice!
Goodbye!

Synchronous vs Asynchronous Callbacks

Synchronous Callback


function processData(data, callback) {
    let processed = data.toUpperCase();
    callback(processed);
}

processData("nodejs", function(result) {
    console.log(result);
});

Output: NODEJS

Asynchronous Callback


setTimeout(function() {
    console.log("Executed after 2 seconds");
}, 2000);

The setTimeout() function executes the callback after the specified delay without blocking the rest of the code.

Callbacks in Node.js Core APIs

Many Node.js APIs, especially those dealing with I/O, use callbacks to return results asynchronously.

Example: Reading a File


const fs = require('fs');

fs.readFile('example.txt', 'utf8', function(err, data) {
    if (err) {
        return console.error(err);
    }
    console.log(data);
});

Error-first Callbacks

Node.js follows the error-first callback convention where the first argument of the callback is reserved for an error object (if any), and the second argument is the data.


function doSomething(callback) {
    const error = false;
    if (error) {
        callback("Something went wrong", null);
    } else {
        callback(null, "Success");
    }
}

doSomething(function(err, result) {
    if (err) {
        return console.error(err);
    }
    console.log(result);
});

Callback Hell

Callback hell refers to the situation where multiple nested callbacks make code difficult to read and maintain.


login(user, function(err, user) {
    if (!err) {
        getProfile(user.id, function(err, profile) {
            if (!err) {
                getFriends(profile.id, function(err, friends) {
                    if (!err) {
                        console.log(friends);
                    }
                });
            }
        });
    }
});

Solutions

  • Modularizing functions
  • Using Promises
  • Using async/await (ES2017+)

What is an Event Emitter?

Node.js uses the EventEmitter class from the events module to handle events. An EventEmitter is an object that emits named events and allows functions (listeners) to be attached and invoked when those events are emitted.

Basic Syntax


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

// Register an event listener
emitter.on('start', () => {
    console.log('Started!');
});

// Emit the event
emitter.emit('start');

Output: Started!

Creating a Custom EventEmitter


const EventEmitter = require('events');

class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter();

myEmitter.on('data', (message) => {
    console.log(`Received: ${message}`);
});

myEmitter.emit('data', 'Hello from event!');

Output: Received: Hello from event!

Common EventEmitter Methods


emitter.on(event, listener)       // Registers a listener
emitter.emit(event, [...args])   // Emits an event
emitter.once(event, listener)    // Registers a one-time listener
emitter.removeListener(event, fn)// Removes a listener
emitter.removeAllListeners()     // Removes all listeners

Example with once()


const emitter = new EventEmitter();

emitter.once('login', () => {
    console.log('User logged in (only once)');
});

emitter.emit('login');
emitter.emit('login');

Output:


User logged in (only once)

Using Parameters with emit()


emitter.on('greet', (name, age) => {
    console.log(`Hello ${name}, you are ${age} years old`);
});

emitter.emit('greet', 'Alice', 30);

Output: Hello Alice, you are 30 years old

Handling Errors with Event Emitters

Node.js throws an error if an 'error' event is emitted without a listener.

Safe Error Handling


const emitter = new EventEmitter();

emitter.on('error', (err) => {
    console.error('Handled error:', err.message);
});

emitter.emit('error', new Error('Something went wrong'));

Real-World Use Cases of Event Emitters

HTTP Server


const http = require('http');

const server = http.createServer((req, res) => {
    res.end('Hello World');
});

server.on('request', (req, res) => {
    console.log(`Request received for ${req.url}`);
});

server.listen(3000);

File Streaming


const fs = require('fs');

const stream = fs.createReadStream('example.txt');

stream.on('data', (chunk) => {
    console.log('New chunk:', chunk.toString());
});

stream.on('end', () => {
    console.log('Reading finished');
});

Combining Callbacks and Event Emitters

You can use both callbacks and event emitters in a single module for more flexibility.

Example


const EventEmitter = require('events');

function fetchData(callback) {
    const emitter = new EventEmitter();

    setTimeout(() => {
        emitter.emit('progress', '25%');
        setTimeout(() => {
            emitter.emit('progress', '50%');
            setTimeout(() => {
                emitter.emit('progress', '100%');
                callback(null, 'Data fetched');
            }, 500);
        }, 500);
    }, 500);

    return emitter;
}

const emitter = fetchData((err, result) => {
    if (err) return console.error(err);
    console.log(result);
});

emitter.on('progress', (percent) => {
    console.log(`Progress: ${percent}`);
});

Best Practices

  • Use descriptive event names
  • Always handle the 'error' event
  • Avoid memory leaks: remove listeners using removeListener() or once()
  • Use async/await or Promises for modern asynchronous code where appropriate
  • Use EventEmitter for streams, chat messages, sensors, and real-time updates

Limitations of EventEmitter

  • Harder to debug than Promises or async/await
  • Listeners can be hard to track in large applications
  • Events are not automatically ordered or scoped

Alternatives to Callbacks and Event Emitters

Promises


function asyncTask() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Task complete');
        }, 1000);
    });
}

asyncTask().then(console.log);

Async/Await


async function run() {
    let result = await asyncTask();
    console.log(result);
}

run();

Callbacks and Event Emitters are powerful tools in the Node.js ecosystem. They allow developers to handle asynchronous operations and events efficiently. While callbacks form the basis of asynchronous programming, event emitters allow for scalable, modular, and decoupled event-driven systems.

Understanding when to use callbacks vs event emitters is critical:

  • Use callbacks when a function performs a single asynchronous operation
  • Use EventEmitters when you want to track and respond to multiple events over time

By mastering these mechanisms, developers can build responsive, high-performance Node.js applications suitable for real-world use cases ranging from web servers to IoT systems and chat applications.

Beginner 5 Hours
Node.js - Callbacks and Event Emitters

Callbacks and Event Emitters in Node.js

Introduction

Node.js is built around asynchronous, non-blocking I/O operations. Two of the most important concepts enabling this behavior are Callbacks and Event Emitters. These two mechanisms allow developers to handle events and operations that may take time, such as reading files, making HTTP requests, or interacting with databases, without freezing the application.

In this document, we will explore:

  • What are Callbacks?
  • Types of Callbacks (Synchronous and Asynchronous)
  • Error-first Callback Convention
  • Callback Hell and Its Solutions
  • What are Event Emitters?
  • Using the EventEmitter class
  • Custom Events and Listeners
  • Real-world examples of Event Emitters
  • Comparisons, best practices, and much more

What is a Callback in Node.js?

A callback is a function passed as an argument to another function. It is called (or "called back") when an operation completes. This approach is foundational to asynchronous programming in Node.js.

Basic Callback Example

function greet(name, callback) { console.log('Hello, ' + name + '!'); callback(); } function sayGoodbye() { console.log('Goodbye!'); } greet('Alice', sayGoodbye);

Output:

Hello, Alice! Goodbye!

Synchronous vs Asynchronous Callbacks

Synchronous Callback

function processData(data, callback) { let processed = data.toUpperCase(); callback(processed); } processData("nodejs", function(result) { console.log(result); });

Output: NODEJS

Asynchronous Callback

setTimeout(function() { console.log("Executed after 2 seconds"); }, 2000);

The setTimeout() function executes the callback after the specified delay without blocking the rest of the code.

Callbacks in Node.js Core APIs

Many Node.js APIs, especially those dealing with I/O, use callbacks to return results asynchronously.

Example: Reading a File

const fs = require('fs'); fs.readFile('example.txt', 'utf8', function(err, data) { if (err) { return console.error(err); } console.log(data); });

Error-first Callbacks

Node.js follows the error-first callback convention where the first argument of the callback is reserved for an error object (if any), and the second argument is the data.

function doSomething(callback) { const error = false; if (error) { callback("Something went wrong", null); } else { callback(null, "Success"); } } doSomething(function(err, result) { if (err) { return console.error(err); } console.log(result); });

Callback Hell

Callback hell refers to the situation where multiple nested callbacks make code difficult to read and maintain.

login(user, function(err, user) { if (!err) { getProfile(user.id, function(err, profile) { if (!err) { getFriends(profile.id, function(err, friends) { if (!err) { console.log(friends); } }); } }); } });

Solutions

  • Modularizing functions
  • Using Promises
  • Using async/await (ES2017+)

What is an Event Emitter?

Node.js uses the EventEmitter class from the events module to handle events. An EventEmitter is an object that emits named events and allows functions (listeners) to be attached and invoked when those events are emitted.

Basic Syntax

const EventEmitter = require('events'); const emitter = new EventEmitter(); // Register an event listener emitter.on('start', () => { console.log('Started!'); }); // Emit the event emitter.emit('start');

Output: Started!

Creating a Custom EventEmitter

const EventEmitter = require('events'); class MyEmitter extends EventEmitter {} const myEmitter = new MyEmitter(); myEmitter.on('data', (message) => { console.log(`Received: ${message}`); }); myEmitter.emit('data', 'Hello from event!');

Output: Received: Hello from event!

Common EventEmitter Methods

emitter.on(event, listener) // Registers a listener emitter.emit(event, [...args]) // Emits an event emitter.once(event, listener) // Registers a one-time listener emitter.removeListener(event, fn)// Removes a listener emitter.removeAllListeners() // Removes all listeners

Example with once()

const emitter = new EventEmitter(); emitter.once('login', () => { console.log('User logged in (only once)'); }); emitter.emit('login'); emitter.emit('login');

Output:

User logged in (only once)

Using Parameters with emit()

emitter.on('greet', (name, age) => { console.log(`Hello ${name}, you are ${age} years old`); }); emitter.emit('greet', 'Alice', 30);

Output: Hello Alice, you are 30 years old

Handling Errors with Event Emitters

Node.js throws an error if an 'error' event is emitted without a listener.

Safe Error Handling

const emitter = new EventEmitter(); emitter.on('error', (err) => { console.error('Handled error:', err.message); }); emitter.emit('error', new Error('Something went wrong'));

Real-World Use Cases of Event Emitters

HTTP Server

const http = require('http'); const server = http.createServer((req, res) => { res.end('Hello World'); }); server.on('request', (req, res) => { console.log(`Request received for ${req.url}`); }); server.listen(3000);

File Streaming

const fs = require('fs'); const stream = fs.createReadStream('example.txt'); stream.on('data', (chunk) => { console.log('New chunk:', chunk.toString()); }); stream.on('end', () => { console.log('Reading finished'); });

Combining Callbacks and Event Emitters

You can use both callbacks and event emitters in a single module for more flexibility.

Example

const EventEmitter = require('events'); function fetchData(callback) { const emitter = new EventEmitter(); setTimeout(() => { emitter.emit('progress', '25%'); setTimeout(() => { emitter.emit('progress', '50%'); setTimeout(() => { emitter.emit('progress', '100%'); callback(null, 'Data fetched'); }, 500); }, 500); }, 500); return emitter; } const emitter = fetchData((err, result) => { if (err) return console.error(err); console.log(result); }); emitter.on('progress', (percent) => { console.log(`Progress: ${percent}`); });

Best Practices

  • Use descriptive event names
  • Always handle the 'error' event
  • Avoid memory leaks: remove listeners using removeListener() or once()
  • Use async/await or Promises for modern asynchronous code where appropriate
  • Use EventEmitter for streams, chat messages, sensors, and real-time updates

Limitations of EventEmitter

  • Harder to debug than Promises or async/await
  • Listeners can be hard to track in large applications
  • Events are not automatically ordered or scoped

Alternatives to Callbacks and Event Emitters

Promises

function asyncTask() { return new Promise((resolve, reject) => { setTimeout(() => { resolve('Task complete'); }, 1000); }); } asyncTask().then(console.log);

Async/Await

async function run() { let result = await asyncTask(); console.log(result); } run();

Callbacks and Event Emitters are powerful tools in the Node.js ecosystem. They allow developers to handle asynchronous operations and events efficiently. While callbacks form the basis of asynchronous programming, event emitters allow for scalable, modular, and decoupled event-driven systems.

Understanding when to use callbacks vs event emitters is critical:

  • Use callbacks when a function performs a single asynchronous operation
  • Use EventEmitters when you want to track and respond to multiple events over time

By mastering these mechanisms, developers can build responsive, high-performance Node.js applications suitable for real-world use cases ranging from web servers to IoT systems and chat applications.

Related Tutorials

Frequently Asked Questions for Node.js

A function passed as an argument and executed later.

Runs multiple instances to utilize multi-core systems.

Reusable blocks of code, exported and imported using require() or import.

nextTick() executes before setImmediate() in the event loop.

Starts a server and listens on specified port.

Node Package Manager β€” installs, manages, and shares JavaScript packages.

A minimal and flexible web application framework for Node.js.

A stream handles reading or writing data continuously.

It processes asynchronous callbacks and non-blocking I/O operations efficiently.

Node.js is a JavaScript runtime built on Chrome's V8 engine for server-side scripting.

An object representing the eventual completion or failure of an asynchronous operation.

require is CommonJS; import is ES6 syntax (requires transpilation or newer versions).

Use module.exports or exports.functionName.

Variables stored outside the code for configuration, accessed using process.env.


MongoDB, often used with Mongoose for schema management.

Describes project details and manages dependencies and scripts.

Synchronous blocks execution; asynchronous runs in background without blocking.

Allows or restricts resources shared between different origins.

Use try-catch, error events, or middleware for error handling.

Provides file system-related operations like read, write, delete.

Using event-driven architecture and non-blocking I/O.

Functions in Express that execute during request-response cycle.

A set of routes or endpoints to interact with server logic or databases.

Yes, it's single-threaded but handles concurrency using the event loop and asynchronous callbacks.

Middleware to parse incoming request bodies, like JSON or form data.

line

Copyrights © 2024 letsupdateskills All rights reserved