askesis

Node.js essentials: 50 cards on the event loop, modules and streams

Node.js fundamentals: the event loop, ESM vs CommonJS, npm, streams, Buffer, fs/promises, process, error handling, worker threads and a basic HTTP server.

Web developmentBeginner50 cardsFree

Practice now

Study without an account

0 of 50 seen0 graduated
What is Node.js?

Scheduled with the default Askesis timings. Progress is saved in this browser.

All 50 cards

  1. 1
    What is Node.js?
    A JavaScript runtime built on the V8 engine and libuv, for running JavaScript outside the browser: servers, CLIs and build tools
  2. 2
    Is Node.js single-threaded?
    Your JavaScript runs on one main thread, but I/O is handed to the operating system and libuv’s thread pool, so waiting on files or the network doesn’t block it
  3. 3
    What is the event loop?
    The loop that takes ready callbacks (finished I/O, due timers, and so on) from queues and runs them one at a time on the main thread whenever the call stack is empty
  4. 4
    The phases of the Node.js event loop, in order
    timers → pending callbacks → poll (I/O) → check (setImmediate) → close callbacks
    The process.nextTick queue and promise microtasks are drained after each callback, between phases too.
  5. 5
    In what order does this CommonJS script print?
    js
    console.log('A')
    setTimeout(() => console.log('B'), 0)
    Promise.resolve().then(() => console.log('C'))
    process.nextTick(() => console.log('D'))
    console.log('E')
    A E D C B: synchronous code first, then the nextTick queue, then promise callbacks, then timers
    In an ES module the top level already runs inside a promise job, so C prints before D.
  6. 6
    setImmediate vs setTimeout(fn, 0)
    setImmediate runs in the check phase right after I/O polling; setTimeout(fn, 0) waits for the timers phase (with a 1 ms minimum)
    Inside an I/O callback setImmediate always fires first; from the main module their order isn’t guaranteed.
  7. 7
    What blocks the event loop?
    Long synchronous work: heavy loops, readFileSync on big files, huge JSON.parse calls, sync crypto, catastrophic regexes. Every other request waits until it finishes
  8. 8
    What runs on libuv’s thread pool?
    File system calls, dns.lookup, CPU-heavy crypto like pbkdf2 and scrypt, and zlib compression; it has 4 threads by default (UV_THREADPOOL_SIZE)
    Network sockets don’t use the pool; they rely on OS mechanisms such as epoll and kqueue.
  9. 9
    Import and export in CommonJS vs ES modules
    js
    // CommonJS
    const { readFile } = require('node:fs/promises')
    module.exports = { load }
    
    // ES modules
    import { readFile } from 'node:fs/promises'
    export { load }
  10. 10
    How does Node decide whether a .js file is CommonJS or ESM?
    ESM if the nearest package.json has "type": "module", CommonJS otherwise; .mjs files are always ESM and .cjs files always CommonJS
    Recent versions also detect import/export syntax in files with no type field, but setting the field is clearer.
  11. 11
    What replaces __dirname in an ES module?
    import.meta.dirname (and import.meta.filename), available since Node 20.11; older code uses fileURLToPath(import.meta.url)
  12. 12
    Why does this ESM import fail?
    js
    import { sum } from './math'
    ES modules need the full file path, extension included: './math.js'. There is no automatic .js or index.js resolution as in require()
  13. 13
    Can CommonJS require() an ES module?
    Yes, since Node 22.12 and 20.19, as long as the module graph has no top-level await; on older versions use await import()
  14. 14
    Where can you use top-level await?
    Only in ES modules, not in CommonJS files
  15. 15
    Why import node:fs instead of fs?
    The node: prefix always loads the built-in module, never an npm package with the same name; some newer built-ins like node:test and node:sqlite require it
  16. 16
    What do ^1.4.2 and ~1.4.2 allow?
    ^1.4.2 allows >=1.4.2 <2.0.0 (minor and patch updates); ~1.4.2 allows >=1.4.2 <1.5.0 (patch updates only)
    Below 1.0 the caret is stricter: ^0.4.2 means <0.5.0.
  17. 17
    npm install vs npm ci
    npm ci deletes node_modules, installs exactly what the lockfile says and fails if it disagrees with package.json; it never rewrites either file, which makes it right for CI
  18. 18
    Should you commit package-lock.json?
    Yes, for applications: it pins the whole dependency tree so every machine and deploy installs the same versions
  19. 19
    dependencies vs devDependencies
    Packages needed at runtime vs tools only needed to build and test, like TypeScript or a test runner; npm install --omit=dev skips the latter
  20. 20
    What does npx do?
    Runs a package’s command-line tool: from node_modules/.bin if it’s installed, otherwise it offers to download it temporarily
    bash
    npx prettier --check .
  21. 21
    The four kinds of Node.js streams
    Readable, Writable, Duplex (both, like a TCP socket) and Transform (a duplex that changes data passing through, like gzip)
  22. 22
    Why use streams instead of reading a whole file?
    They process data chunk by chunk, so memory stays flat even for gigabyte files, and output can start before the input has finished
  23. 23
    What is backpressure?
    A slow writer telling a fast reader to wait: write() returns false when the buffer is full, and you should pause until the 'drain' event
    pipeline() and .pipe() handle backpressure for you.
  24. 24
    Gzip a file with streams
    js
    import { pipeline } from 'node:stream/promises'
    import { createReadStream, createWriteStream } from 'node:fs'
    import { createGzip } from 'node:zlib'
    
    await pipeline(createReadStream('app.log'), createGzip(), createWriteStream('app.log.gz'))
  25. 25
    Why prefer pipeline() over .pipe()?
    pipeline() forwards errors from any stage and destroys every stream on failure; .pipe() doesn’t propagate errors, which can leak file handles
  26. 26
    Read a stream chunk by chunk without event handlers
    js
    for await (const chunk of stream) {
      process(chunk)
    }
    Readable streams are async iterables.
  27. 27
    What is a Buffer?
    A fixed-size chunk of raw binary data; Buffer is a subclass of Uint8Array with helpers for encodings like UTF-8, hex and base64
  28. 28
    What do these return?
    js
    Buffer.byteLength('é')
    'é'.length
    2 and 1: the string is 2 bytes in UTF-8 but 1 UTF-16 code unit
  29. 29
    Buffer.alloc(n) vs Buffer.allocUnsafe(n)
    alloc fills the memory with zeros; allocUnsafe is faster but may contain old data, so overwrite it completely before use
  30. 30
    Encode and decode base64
    js
    Buffer.from('hi').toString('base64')       // 'aGk='
    Buffer.from('aGk=', 'base64').toString('utf8') // 'hi'
  31. 31
    Read a text file with fs/promises
    js
    import { readFile } from 'node:fs/promises'
    
    const text = await readFile('notes.txt', 'utf8')
    Without an encoding you get a Buffer.
  32. 32
    Why avoid readFileSync in a web server?
    It blocks the event loop, so every other request waits; it’s fine at startup or in short CLI scripts
  33. 33
    Create a nested directory
    js
    await mkdir('data/cache/images', { recursive: true })
    With recursive: true it also succeeds if the directory already exists.
  34. 34
    How should you check that a file exists before reading it?
    Don’t: just read it and handle the error, since the file can change between the check and the read. A missing file fails with err.code === 'ENOENT'
  35. 35
    What is process.argv for this command?
    bash
    node app.js --port 3000
    [pathToNode, pathToApp, '--port', '3000']: your arguments start at index 2, so use process.argv.slice(2)
    util.parseArgs parses flags without a library.
  36. 36
    What type are process.env values?
    Always strings (or undefined): PORT=3000 gives '3000', and DEBUG=false gives the truthy string 'false'
  37. 37
    Load a .env file without the dotenv package
    bash
    node --env-file=.env app.js
    Available since Node 20.6; process.loadEnvFile() does the same from code.
  38. 38
    process.exit(1) vs process.exitCode = 1
    process.exit() stops immediately, possibly before pending writes or async work finish; setting exitCode lets the process end naturally with that code
  39. 39
    process.cwd() vs import.meta.dirname
    cwd() is the directory the process was started from; dirname is the folder of the current module file. Resolve files relative to the module
  40. 40
    What is an error-first callback?
    The classic Node callback style: the first argument is an error (or null) and the result comes second
    js
    fs.readFile(path, (err, data) => {
      if (err) return handle(err)
      use(data)
    })
  41. 41
    What happens on an unhandled promise rejection?
    Since Node 15 the process crashes with exit code 1 by default; always await promises or attach .catch()
  42. 42
    What should a process.on('uncaughtException') handler do?
    Log the error, then exit. The process may be in an inconsistent state, so resuming is unsafe; let a supervisor restart it
  43. 43
    What happens when an EventEmitter emits 'error' with no listener?
    The error is thrown, which usually crashes the process; always handle 'error' on streams, sockets and other emitters
  44. 44
    Are EventEmitter listeners called asynchronously?
    No. emit() calls every listener synchronously, in the order they were added, before it returns
  45. 45
    What are worker threads for?
    Running CPU-heavy JavaScript in parallel; each worker has its own V8 instance and event loop and talks to the main thread with postMessage
    js
    import { Worker } from 'node:worker_threads'
    
    const worker = new Worker(new URL('./hash.js', import.meta.url), { workerData: file })
    worker.on('message', (digest) => console.log(digest))
  46. 46
    Do worker threads speed up I/O-heavy code?
    Not really: Node’s async I/O already avoids blocking. Workers help with CPU-bound tasks like image processing, parsing or hashing
  47. 47
    Write a minimal HTTP server
    js
    import { createServer } from 'node:http'
    
    createServer((req, res) => {
      res.writeHead(200, { 'Content-Type': 'text/plain' })
      res.end('Hello')
    }).listen(3000)
    Forget res.end() and the request hangs until it times out.
  48. 48
    Read a JSON request body with plain node:http
    js
    const chunks = []
    for await (const chunk of req) chunks.push(chunk)
    const body = JSON.parse(Buffer.concat(chunks).toString())
    req is a Readable stream; the body arrives in chunks.
  49. 49
    Does Node have fetch built in?
    Yes, globally since Node 18 (stable since 21), along with Request, Response, Headers and FormData; it’s built on undici
  50. 50
    Can Node run a .ts file directly?
    Yes, since Node 22.18 and 23.6 it strips type annotations by default. It doesn’t type-check, and syntax that needs transforming, like enum, requires --experimental-transform-types

Frequently asked questions

Which Node.js version does this deck target?

Current LTS releases. Cards note when a feature is recent, such as require() of ES modules (22.12), import.meta.dirname (20.11) and built-in TypeScript type stripping (22.18).

Do I need to know JavaScript first?

Yes. The deck assumes you know functions, promises and async/await; our JavaScript essentials deck covers those.

Does it cover Express or other frameworks?

No. It sticks to Node.js itself, which every framework builds on: the event loop, modules, streams and the built-in http module.