Skip to main content

Concept Atlas

Break down the runtime concept behind the bug.

Each concept explains the mental model, common wrong assumption, real-world signals, debug questions, and the exact visual cases to practice.

8 concept breakdowns
Concept breakdown

Why output order changes

Synchronous code runs first, microtasks drain before timers, and Node adds extra queues like nextTick and check.

Mental model: Do not read async code only from top to bottom. First ask where each callback is placed: current stack, microtask queue, timer queue, Node nextTick, I/O, or check.
Common wrong assumption: Assuming setTimeout(..., 0) means run immediately, or assuming promises and timers share the same queue.
Real-world signals
  • - Console output differs from source order.
  • - Tests need mysterious waits.
  • - Cleanup runs later than expected.
  • - setImmediate and setTimeout order feels inconsistent.
Debug questions
  • - What runs on the current stack?
  • - Which callbacks enter microtasks?
  • - Which callbacks enter timers or Node-specific queues?
  • - Is this browser behavior or Node behavior?
Concept breakdown

Why await did not wait

await pauses only the current async function. Missing await, async forEach, and unreturned promises let code continue too early.

Mental model: Every async operation must be either awaited, returned, or intentionally allowed to run in the background with error handling.
Common wrong assumption: Calling an async function like a normal function and assuming later code waits for it.
Real-world signals
  • - Success UI appears before save finishes.
  • - Bulk work logs complete too early.
  • - A test finishes before setup completes.
  • - The next .then receives undefined.
Debug questions
  • - Is the promise awaited or returned?
  • - Is async work inside forEach?
  • - Does every .then return the value needed later?
  • - Where are errors caught?
Concept breakdown

Why APIs become slower than expected

Independent I/O should usually start together. Sequential await turns total latency into the sum of every call.

Mental model: Separate dependency from timing. If two requests do not depend on each other, start both before awaiting results.
Common wrong assumption: Writing clean-looking sequential await code that quietly serializes independent database/API calls.
Real-world signals
  • - Endpoint latency is close to call A + call B + call C.
  • - Dashboard loads after the slowest plus every previous request.
  • - Serverless function duration is higher than any single dependency.
Debug questions
  • - Which requests truly depend on previous results?
  • - Can independent promises start before the first await?
  • - Should failure be all-or-nothing or partial?
  • - Do timeouts cancel the slow work?
Concept breakdown

Why the app freezes

Synchronous CPU work blocks the main thread or Node event loop, delaying input, paint, timers, and callbacks.

Mental model: Async I/O can wait elsewhere, but CPU-heavy JavaScript runs on the current thread unless you chunk it or move it to a worker.
Common wrong assumption: Wrapping CPU-heavy work in async/await and expecting it to stop blocking.
Real-world signals
  • - Clicks feel ignored.
  • - Timers fire late.
  • - Animations freeze.
  • - One request delays unrelated Node callbacks.
Debug questions
  • - Is there a long task over 50ms?
  • - Is JSON parsing, sorting, or transforming huge data?
  • - Can work be chunked?
  • - Should a worker thread handle it?
Concept breakdown

Why memory keeps growing

Garbage collection frees unreachable objects. Timers, listeners, caches, closures, and streams can keep objects reachable.

Mental model: Memory leaks are usually reference leaks. Ask who still points to the object after the feature should be finished.
Common wrong assumption: Assuming garbage collection frees objects that are still stored in a cache, listener, closure, or active timer.
Real-world signals
  • - Heap grows after repeated navigation.
  • - Listener count increases.
  • - Polling continues after a component disappears.
  • - Large files spike memory.
Debug questions
  • - What reference keeps this object alive?
  • - Is there matching cleanup?
  • - Is the cache bounded?
  • - Does stream failure close resources?
Concept breakdown

Why Node gets slow under async load

Node has one main JS thread plus limited backend capacity. fs, crypto, zlib, and DNS-like work can queue behind each other.

Mental model: Async does not mean infinite. Some async APIs share worker capacity, and CPU-heavy JS needs workers or separate processes.
Common wrong assumption: Starting a huge Promise.all batch and expecting every async task to run with unlimited capacity.
Real-world signals
  • - Crypto slows file work.
  • - DNS or outbound requests look delayed.
  • - Compression bursts hurt unrelated endpoints.
  • - p95 latency spikes under batch jobs.
Debug questions
  • - Which APIs use worker-pool capacity?
  • - Is Promise.all starting too much at once?
  • - Can concurrency be limited?
  • - Should heavy work move out of request paths?
Concept breakdown

Why streams hang or use too much memory

Streams are chunked data flow. Backpressure and error cleanup keep the fast side from overwhelming the slow side.

Mental model: A readable side can produce faster than a writable side can consume. Good stream code pauses, resumes, and cleans up on failure.
Common wrong assumption: Manually wiring data events, ignoring write() returning false, or using pipe chains without centralized error handling.
Real-world signals
  • - Uploads hang.
  • - Large downloads use too much memory.
  • - A failed transform leaves a socket open.
  • - write buffers keep growing.
Debug questions
  • - Is backpressure respected?
  • - Does every stream have error handling?
  • - Would pipeline simplify cleanup?
  • - Is the whole file being read into memory?
Concept breakdown

Why one failure breaks everything

Promise combinators encode failure behavior. all fails fast, allSettled keeps outcomes, any waits for first success, and race settles first.

Mental model: Pick the combinator based on product behavior, not habit. Decide whether partial success is useful before choosing Promise.all.
Common wrong assumption: Using Promise.all for dashboards, imports, or optional data where one failure should not hide every success.
Real-world signals
  • - One widget blanks a whole dashboard.
  • - Batch uploads look fully failed.
  • - A fast failed mirror beats a slower success.
  • - Timeouts do not cancel slow work.
Debug questions
  • - Should one failure stop everything?
  • - Do you need every result status?
  • - Should first failure count as winner?
  • - Does timeout cancel the slow operation?