DEV Community

Cover image for 10 JavaScript Concepts Every Node.js Developer Should Know
Lucy Muturi for Syncfusion, Inc.

Posted on • Originally published at syncfusion.com on

10 JavaScript Concepts Every Node.js Developer Should Know

TL;DR: Struggling to adapt your JavaScript skills to Node.js? You’re not alone. Node.js uses JavaScript, but its server-side environment changes how core concepts like globals, events, and modules work. This guide breaks down the 10 essential JavaScript concepts every Node.js developer needs to master, starting with key differences from browser-based JavaScript to help you avoid pitfalls and write cleaner, more reliable code. Whether you’re debugging unexpected behavior or optimizing performance, these insights will help you build scalable, efficient server-side applications.

JavaScript is one of the most popular programming languages worldwide, especially for web development. Its flexibility and dynamic nature make it easy to learn and widely adopted.

This popularity paved the way for Node.js, a powerful runtime environment that lets developers run JavaScript on the server side using the Chrome V8 engine. While Node.js uses the same JavaScript syntax, its execution environment introduces key differences.

This guide covers the top 10 concepts you need to understand to leverage JavaScript effectively in Node.js, ensuring you avoid runtime surprises and build robust applications.

1. Global scope and runtime differences

In browser-based JavaScript, the global object is window, which provides access to Web APIs like setTimeout, setInterval, and DOM manipulation via the document object. In Node.js, the global object is global, and it operates differently, with unique scope and accessibility rules. It can access server-specific APIs like setImmediate, nextTick, and others, but lacks browser-specific objects like alert, prompt, confirm, or document.

// JavaScript
alert('syncfusion');
localStorage.setItem("syncfusion", "hello");

// Nodejs
// alert is not defined
// localStorage is not defined
Enter fullscreen mode Exit fullscreen mode

This distinction is critical because developers often expect browser-like behavior in Node.js. For example, using this in the global scope of a Node.js module refers to module.exports, not global, which can lead to bugs when accessing global variables or methods.

//Browser
console.log(this); // window

//Nodejs
console.log(this); // {}
console.log(global); // [global object]
Enter fullscreen mode Exit fullscreen mode

DOM access

Node.js runs outside the browser, so it cannot access the DOM or BOM APIs. This is a common stumbling block for frontend developers moving to backend JavaScript.

// JavaScript
document.getElementById('syncfusion').innerText = "hello";

// Nodejs
// ReferenceError: document is not defined
Enter fullscreen mode Exit fullscreen mode

2. Event loop and timers

Node.js introduces setImmediate() and nextTick(), but at different phases of the event loop. nextTick() executes immediately after the current action but before other I/O events, whereas setImmediate() executes in the next event loop cycle after an I/O operation. setImmediate() is only supported in Node.js.

//Nodejs
process.nextTick(() => console.log('nextTick'));
setImmediate(() => console.log('setImmediate'));
console.log('main');

// Output:
// main
// nextTick
// setImmediate
Enter fullscreen mode Exit fullscreen mode

Understanding these timers is crucial for managing asynchronous tasks and optimizing performance in Node.js applications.

3. Asynchronous programming with promises and Async/Await

JavaScript is single threaded but supports asynchronous operations via promises and async/await. These are essential for writing non-blocking server-side code.

While Node.js is event-driven, so is async programming at its core. To wait for the existing operations to finish and move forward, we rely on promises or async-await.

Promises

Promises allow you to handle asynchronous operations by invoking callback functions when an action completes, but they can lead to callback hell with nested callbacks.

// Example: promise
await fetch('https://api.example.com/data')
    .then(res => res.json())
    .then(json => {
        console.log(json);
    }).catch(err => {
        console.error(err);
    }).finally(() => {
        console.log('action completed');
    });
Enter fullscreen mode Exit fullscreen mode

Async/Await

Async/await is an alternative to promises, which helps us execute asynchronous tasks synchronously using the await keyword.

// Example: async/await
async function fetchData() {
    try{
        const response = await fetch('https://api.example.com/data');
        const json = await response.json();
        console.log(json);
    }catch(e){
        console.error(e);
    }finally(){
        console.log('action completed');
    }
}
Enter fullscreen mode Exit fullscreen mode

Node.js supports fetch() natively from version 18 onward, aligning browser and server HTTP request APIs. Check the official documentation for more details.

4. Handling large data with streams

Streams are essential for handling large files or network responses without loading everything into memory. This is especially useful for performance optimization.

const fs = require('fs');
fs.createReadStream('file.txt')
  .pipe(fs.createWriteStream('copy.txt'));
Enter fullscreen mode Exit fullscreen mode

You can also listen to stream events:

const fs = require('fs');
const filePath = largeFile.txt';
const readableStream = fs.createReadStream(filePath, { encoding: 'utf8' });
readableStream.on('data', (chunk) => { 
    console.log(`${chunk.length} characters of data is received.`); 
}

readableStream.on('end', () => { 
    console.log(Completed reading the file.');
});

readableStream.on('error', (error) => { 
    console.error(`An error occurred: ${error.message}`); 
});
Enter fullscreen mode Exit fullscreen mode

5. Modules: CommonJS and ES Modules

Modules are fundamental for writing scalable and maintainable JavaScript applications. They allow you to isolate logic into reusable pieces, reducing redundancy and improving organization. Node.js supports two module systems, each with distinct syntax and use cases.

1. CommonJS

CommonJS is the traditional Node.js module system, using require() to import and module.exports to export.

// export
// example.js
function add(a, b) {
    return a + b;
}

module.exports = {
    add
};

// import
const {add} = require('./math');
console.log(add(2, 3)); // 5
Enter fullscreen mode Exit fullscreen mode

CommonJS is used by default in .js files and is widely supported across Node.js versions.

2. ES Modules

ES Modules use import and export syntax, aligning with browser-based JavaScript.

// example.mjs
export function add(a, b) {
    return a + b;
}

export default function log(message) {
    console.log(`[LOG]: ${message}`);
}

import { add} from './example.mjs';
log('App initialized'); // [LOG]: App initialized
console.log(add(2, 3)); // 5
Enter fullscreen mode Exit fullscreen mode

ES Modules are supported for .mjs files or when type: module is set in package.json.

Event handling with EventEmitter

Node.js excels at event-driven programming, using the EventEmitter class to create and listen to custom events, unlike browser JavaScript’s DOM-based event listeners. This is useful for decoupling logic and building scalable systems.

window.addEventListener('click', (e) => {
    console.log('clicked', e.target);
});

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

emitter.on('log', msg => console.log(msg));
emitter.emit('log', 'An event has occurred!');
Enter fullscreen mode Exit fullscreen mode

This approach allows passing data to multiple parts of an application efficiently.

7. Multi-threading with worker threads

For CPU-intensive tasks, Node.js supports multi-threading via worker_threads, offloading heavy computations without blocking the main thread. This contrasts with browser-based web workers.

const { Worker } = require('worker_threads');
new Worker('./heavyComputation.js');
Enter fullscreen mode Exit fullscreen mode

8. Custom error handling

Creating custom error classes in Node.js improves error clarity and control, especially in large applications.

class NotFoundError extends Error {
    constructor(message) {
        super(message);
        this.name = "NotFoundError";
        this.status = 404;
    }
}
Enter fullscreen mode Exit fullscreen mode

9. Closures and higher-order functions

Closures and higher-order functions are core JavaScript concepts that shine in Node.js for creating middleware, managing state, and enabling functional programming patterns.

Closures

Closures allow functions to retain access to their lexical scope, which is useful for creating Higher-order functions and middleware patterns.

function outer() {
    let count = 0;
    return function inner() {
        return ++count;
    };
}

const counter = outer();
console.log(counter()); // 1
console.log(counter()); // 2
Enter fullscreen mode Exit fullscreen mode

Higher-order functions

Higher-order functions take or return other functions, enabling functional programming patterns.

const add = x => y => x + y;
const add5 = add(5);
console.log(add5(3)); // 8
Enter fullscreen mode Exit fullscreen mode

These are particularly useful in Node.js for building reusable middleware and managing async workflows.

10. Understanding ‘this’ in JavaScript functions

The this is a special keyword in JavaScript whose value is decided at runtime depending upon the context in which it was called.

Normal function

In a normal function, this refers to the global object in Node.js.

function example(){
    // in Node.js, this refers to the global object
    console.log(this === global);
}

example();
// true
Enter fullscreen mode Exit fullscreen mode

As a method

When invoked as a method, this refers to the object.

const obj = {
    blog: "syncfusion",
    displayBlog: function (){
        console.log(this.blog);
    }
};

obj.displayBlog();
// "syncfusion"
Enter fullscreen mode Exit fullscreen mode

As a constructor

When used as a constructor, this refers to the new instance.

function Example(blog){
    this.blog = blog;
    this.displayBlog = function(){
        console.log(this.blog);
    };
};

const example = new Example("syncfusion");
example.displayBlog(); // "syncfusion"
Enter fullscreen mode Exit fullscreen mode

Runtime binding

You can update this at runtime using call or apply.

const obj = {
    blog: syncfusion,
};

function example(name) {
    console.log(`${name} runs ${this.blog}`);
};

example.call(obj, 'Daniel');
// "Daniel runs syncfusion"

example.apply(obj, ['Daniel']);
// "Daniel runs syncfusion"
Enter fullscreen mode Exit fullscreen mode

The only difference between call and apply is that call accepts individual values, whereas apply accepts an array of values along with the context as the first argument.

Permanent binding:

The bind method permanently sets this.

const obj = {
    name: 'Daniel',
};

function example(blog) {
    console.log(`${this.name} runs ${blog}`);
};

const bounded = example.bind(obj);

bounded('MDN');
// "Daniel runs MDN"
Enter fullscreen mode Exit fullscreen mode

Conclusion

Node.js lets you write server-side code using JavaScript, but its environment introduces key differences that can trip up even experienced developers. By mastering these 10 JavaScript concepts, especially how they behave in Node.js, you’ll avoid common pitfalls, write cleaner code, and build scalable backend applications. Try applying these concepts in your next Node.js project and explore the Node.js documentation for deeper insights!

Related Blogs

This article was originally published at Syncfusion.com.

Top comments (0)