Handle work that takes time without freezing your app. Master callbacks, Promises, async/await, the timer functions, and the EventEmitter pattern.
Why: some tasks take time — reading a file, calling a server, waiting on a database. Node does not sit and wait ("block"); it starts the task, keeps running other code, and comes back when the result is ready.
Why: a callback is a function you hand to another function to be called later, when the work is done. It is the oldest async style. Many callbacks call back with an error first, then the result (the "error-first" convention).
// app.js
import { readFile } from 'node:fs'
readFile('note.txt', 'utf8', (err, data) => {
if (err) {
console.error('Could not read file:', err.message)
return
}
console.log('File says:', data)
})
console.log('This runs first — readFile finishes later')Why: a Promise is an object representing a value that will arrive later. It is cleaner than nesting callbacks. You attach .then() for success and .catch() for errors. Most modern APIs return Promises.
// app.js
import { readFile } from 'node:fs/promises'
readFile('note.txt', 'utf8')
.then((data) => {
console.log('File says:', data)
})
.catch((err) => {
console.error('Failed:', err.message)
})Why: async/await lets you write Promise-based code that reads top-to-bottom like normal code. Put "await" before a Promise to pause until it resolves, and wrap it in try/catch to handle errors. This is the preferred style today.
// app.js
import { readFile } from 'node:fs/promises'
async function showFile() {
try {
const data = await readFile('note.txt', 'utf8')
console.log('File says:', data)
} catch (err) {
console.error('Failed:', err.message)
}
}
showFile()Why: these schedule code to run later. setTimeout runs once after a delay; setInterval runs repeatedly; setImmediate runs right after the current work; process.nextTick runs even sooner, before anything else queued. The last two are advanced — reach for setTimeout/setInterval most of the time.
// app.js
setTimeout(() => console.log('runs once after 1s'), 1000)
const id = setInterval(() => console.log('every 2s'), 2000)
// stop it later with clearInterval(id)
setImmediate(() => console.log('right after current work'))
process.nextTick(() => console.log('even sooner'))Why: an EventEmitter lets one part of your code announce that something happened ("emit" an event) and other parts react to it ("on" / listen). Much of Node is built on this pattern — servers, streams, and more.
// app.js
import { EventEmitter } from 'node:events'
const orders = new EventEmitter()
// listen for the 'placed' event
orders.on('placed', (item) => {
console.log('New order:', item)
})
// announce it
orders.emit('placed', 'Coffee') // New order: Coffee