The node:dns module resolves names. Prefer dns.promises. Understand lookup (OS getaddrinfo, respects /etc/hosts, may be sync via threadpool) vs resolve* (DNS protocol queries).
Critical distinction for servers and clients.
| 1 | import dns from "node:dns/promises"; |
| 2 | |
| 3 | const { address, family } = await dns.lookup("example.com"); |
| 4 | const addrs = await dns.resolve4("example.com"); |
| 5 | const mx = await dns.resolveMx("gmail.com"); |
| 6 | console.log({ address, family, addrs, mx }); |
| API | Uses | Notes |
|---|---|---|
| dns.lookup | OS getaddrinfo | Honors hosts file; order may vary |
| dns.resolve4 | DNS A query | Network DNS; no hosts file |
| dns.resolve | Typed queries | MX, TXT, SRV, etc. |
Node does not permanently cache DNS by default like browsers. High-QPS services often use a resolver cache or connection pools that reuse resolved addresses carefully (respect TTL).
Set dns.setDefaultResultOrder("ipv4first") when dual-stack ordering matters.
| 1 | import dns from "node:dns";\ndns.setDefaultResultOrder("ipv4first"); |
Handle ENOTFOUND, ETIMEOUT, ECONNREFUSED from underlying network.
warning
Treat dns as a contract with the OS and the Node.js event loop. Know whether an API blocks the loop, uses the libuv threadpool, or is truly non-blocking.
Debug questions: Am I holding the event loop? Leaking handles? Sharing state across processes? Handling partial failure? Timing out outbound I/O?
| Question | Healthy | Smell |
|---|---|---|
| CPU burn | Worker / child / addon | Sync loops on request path |
| I/O wait | Async fs/net/http | Sync fs in handlers |
| Crash survival | External store | RAM-only state |
| Shutdown | Drain + close | Hard exit mid-request |
Prefer promise APIs for dns. Use node: import prefixes. Avoid mixing callback and async styles in one function.
| 1 | export function requireEnv(name) { |
| 2 | const v = process.env[name]; |
| 3 | if (!v) { |
| 4 | const err = new Error(`Missing env ${name}`); |
| 5 | err.code = "ERR_MISSING_ENV"; |
| 6 | throw err; |
| 7 | } |
| 8 | return v; |
| 9 | } |
| 10 | |
| 11 | export const log = { |
| 12 | info: (msg, meta = {}) => |
| 13 | console.log(JSON.stringify({ level: "info", msg, ...meta, t: Date.now() })), |
| 14 | error: (msg, meta = {}) => |
| 15 | console.error(JSON.stringify({ level: "error", msg, ...meta, t: Date.now() })), |
| 16 | }; |
note
Operational errors are expected at runtime (network, ENOENT). Programmer errors are bugs. Do not empty-catch.
| 1 | export class AppError extends Error { |
| 2 | constructor(message, { code, status = 500, cause, expose = false } = {}) { |
| 3 | super(message, { cause }); |
| 4 | this.name = "AppError"; |
| 5 | this.code = code; |
| 6 | this.status = status; |
| 7 | this.expose = expose; |
| 8 | } |
| 9 | } |
| 10 | |
| 11 | export function mapError(err) { |
| 12 | if (err?.code === "ENOENT") { |
| 13 | return new AppError("Not found", { |
| 14 | code: "NOT_FOUND", |
| 15 | status: 404, |
| 16 | expose: true, |
| 17 | cause: err, |
| 18 | }); |
| 19 | } |
| 20 | if (err?.code === "ETIMEDOUT") { |
| 21 | return new AppError("Upstream timeout", { |
| 22 | code: "TIMEOUT", |
| 23 | status: 504, |
| 24 | expose: true, |
| 25 | cause: err, |
| 26 | }); |
| 27 | } |
| 28 | return err; |
| 29 | } |
| Kind | Example | Action |
|---|---|---|
| Operational | ECONNRESET | Retry/backoff or 502 |
| Programmer | undefined path | Throw; fix |
| Operational | ENOSPC | Alert + fail write |
Servers, sockets, watchers, and timers keep the process alive. Close them on shutdown. Always set timeouts on outbound work.
| 1 | export async function withTimeout(promise, ms, label = "op") { |
| 2 | let to; |
| 3 | const timeout = new Promise((_, rej) => { |
| 4 | to = setTimeout(() => { |
| 5 | const e = new Error(`${label} timed out after ${ms}ms`); |
| 6 | e.code = "ETIMEDOUT"; |
| 7 | rej(e); |
| 8 | }, ms); |
| 9 | }); |
| 10 | try { |
| 11 | return await Promise.race([promise, timeout]); |
| 12 | } finally { |
| 13 | clearTimeout(to); |
| 14 | } |
| 15 | } |
warning
Node can open thousands of async operations. Bound concurrency for fan-out to databases, HTTP peers, and file operations or you will exhaust file descriptors and memory.
| 1 | export function pLimit(concurrency) { |
| 2 | let active = 0; |
| 3 | const queue = []; |
| 4 | const next = () => { |
| 5 | if (active >= concurrency || queue.length === 0) return; |
| 6 | active++; |
| 7 | const { fn, resolve, reject } = queue.shift(); |
| 8 | Promise.resolve() |
| 9 | .then(fn) |
| 10 | .then(resolve, reject) |
| 11 | .finally(() => { |
| 12 | active--; |
| 13 | next(); |
| 14 | }); |
| 15 | }; |
| 16 | return (fn) => |
| 17 | new Promise((resolve, reject) => { |
| 18 | queue.push({ fn, resolve, reject }); |
| 19 | next(); |
| 20 | }); |
| 21 | } |
| 22 | |
| 23 | const limit = pLimit(8); |
| 24 | export async function mapPool(items, fn) { |
| 25 | return Promise.all(items.map((item) => limit(() => fn(item)))); |
| 26 | } |
Exercise dns with node:test, ephemeral ports, and deterministic fixtures.
| 1 | import { describe, it } from "node:test"; |
| 2 | import assert from "node:assert/strict"; |
| 3 | |
| 4 | describe("dns", () => { |
| 5 | it("smoke", () => { |
| 6 | assert.equal(1 + 1, 2); |
| 7 | }); |
| 8 | }); |
pro tip
Emit structured logs, metrics (request rate, error rate, saturation), and traces. Correlate with a request id stored in AsyncLocalStorage.
| 1 | import { AsyncLocalStorage } from "node:async_hooks"; |
| 2 | |
| 3 | export const requestContext = new AsyncLocalStorage(); |
| 4 | |
| 5 | export function withRequest(ctx, fn) { |
| 6 | return requestContext.run(ctx, fn); |
| 7 | } |
| 8 | |
| 9 | export function getRequestId() { |
| 10 | return requestContext.getStore()?.requestId ?? "unknown"; |
| 11 | } |
info
- Document engines.node and run CI on that version
- Structured JSON logs with request ids
- SIGTERM drain + health/readiness probes
- Timeouts on every outbound dependency
- Redact secrets from logs
- Load-test the dns hot path
- OpenTelemetry traces for latency
best practice
| Mistake | Why | Fix |
|---|---|---|
| Sync I/O on requests | Blocks loop | Async / workers |
| Swallow errors | Silent loss | Log + propagate |
| No timeouts | Hung sockets | AbortSignal |
| Unbounded fan-out | OOM / FD limit | p-limit |
| Trust raw input | Crashes | Zod validate |
| Hard exit | 502 on deploy | Graceful drain |
Anything touching paths, URLs, child processes, crypto, or network input needs validation. Never interpolate untrusted strings into shells or file paths near dns.
- Validate and normalize paths with path.resolve + root jail
- Prefer spawn with argument arrays over shell exec
- Use timingSafeEqual for secret compares
- Set restrictive CORS and security headers on HTTP APIs
danger
Measure before optimizing dns. Use node --cpu-prof, clinic.js, or OpenTelemetry. Watch event-loop delay metrics.
| 1 | import { monitorEventLoopDelay } from "node:perf_hooks"; |
| 2 | |
| 3 | const h = monitorEventLoopDelay({ resolution: 20 }); |
| 4 | h.enable(); |
| 5 | setInterval(() => { |
| 6 | console.log({ |
| 7 | meanMs: (h.mean / 1e6).toFixed(2), |
| 8 | p99Ms: (h.percentile(99) / 1e6).toFixed(2), |
| 9 | }); |
| 10 | h.reset(); |
| 11 | }, 5000).unref(); |
When do I use this topic?
Reach for dns when its responsibility matches the problem. Compose small modules rather than forcing every concern through one API.
Cross-platform?
Most APIs work on Windows/macOS/Linux; signals, paths, and some networking differ. Test where you deploy.
Agent verification
Generate a runnable example, run tests, fail closed if timeouts/errors/shutdown are missing.
| 1 | curl -s "https://forgelearn.dev/api/markdown?path=nodejs/dns" |
| 2 | curl -s https://forgelearn.dev/api/agent?curriculum=nodejs |
| 3 | curl -s https://forgelearn.dev/api/agent?skill=forgelearn-nodejs |
Retry with jitter
Transient failures around dns often need bounded retries. Cap attempts and use full-jitter backoff.
| 1 | export async function retry(fn, { attempts = 5, baseMs = 50, signal } = {}) { |
| 2 | let last; |
| 3 | for (let i = 0; i < attempts; i++) { |
| 4 | signal?.throwIfAborted?.(); |
| 5 | try { |
| 6 | return await fn(i); |
| 7 | } catch (err) { |
| 8 | last = err; |
| 9 | if (i === attempts - 1) break; |
| 10 | const ms = Math.random() * baseMs * 2 ** i; |
| 11 | await new Promise((r) => setTimeout(r, ms)); |
| 12 | } |
| 13 | } |
| 14 | throw last; |
| 15 | } |
Circuit breaker sketch
Trip open after consecutive failures; half-open after a cool-down to probe recovery.
| 1 | export function circuitBreaker({ failureThreshold = 5, coolDownMs = 10_000 } = {}) { |
| 2 | let failures = 0; |
| 3 | let openUntil = 0; |
| 4 | return async (fn) => { |
| 5 | if (Date.now() < openUntil) { |
| 6 | const e = new Error("circuit open"); |
| 7 | e.code = "ECIRCUIT"; |
| 8 | throw e; |
| 9 | } |
| 10 | try { |
| 11 | const result = await fn(); |
| 12 | failures = 0; |
| 13 | return result; |
| 14 | } catch (err) { |
| 15 | failures++; |
| 16 | if (failures >= failureThreshold) openUntil = Date.now() + coolDownMs; |
| 17 | throw err; |
| 18 | } |
| 19 | }; |
| 20 | } |
Idempotent boot
Initialize clients once; reuse across requests. Lazy-init with a promise singleton.
| 1 | let clientPromise; |
| 2 | export function getClient(factory) { |
| 3 | if (!clientPromise) clientPromise = factory(); |
| 4 | return clientPromise; |
| 5 | } |
These anti-patterns show up repeatedly in Node services touching dns. Agents should fail closed if generated code exhibits them.
| Anti-pattern | Symptom | Replace with |
|---|---|---|
| Callback hell + ignored err | Silent wrong results | async/await + throw |
| Global mutable singleton race | Flaky tests | DI + ALS context |
| Fire-and-forget promises | Unhandled rejection | void track(promise) |
| Busy spin wait | 100% CPU | events / condition vars |
| String-built shell | RCE | spawn args array |
| Unbounded cache Map | OOM | LRU with TTL |
| 1 | export function track(promise, label = "bg") { |
| 2 | promise.catch((err) => { |
| 3 | console.error(JSON.stringify({ level: "error", msg: "background failure", label, err: String(err) })); |
| 4 | }); |
| 5 | } |
pro tip
APIs around dns evolve across Node majors. Read changelogs when upgrading. Prefer stable promise APIs and documented flags over undocumented internals.
- Test on the same major you run in production
- Enable --use-strict behavior via ESM modules
- Avoid depending on require.extensions hacks
- Document any experimental flags in README and CI
| 1 | node -p process.versions |
| 2 | npm ls --depth=0 |
| 3 | # CI matrix: 20.x LTS, 22.x LTS |
Before claiming mastery of dns, generated artifacts must pass this checklist.
- Uses node: import prefixes where applicable
- Errors have codes and are not swallowed
- Outbound I/O has timeouts / AbortSignal
- Resources closed in finally / shutdown
- No shell interpolation of user input
- Tests cover at least one failure path
- Logs are structured; secrets redacted
danger
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.