JavaScript async is the thing under the hood of every app we ship now. Ajax everywhere, Node on the server, timers, events, the whole party. We glue code to a clock that never stops. If you have felt the pain of a callback pyramid, swallowed an error without noticing, or tried to coordinate three XHRs while the UI stays smooth, this one is for you. The short version: callbacks still work, promises make composition feel sane, and a few patterns save your nights and your diff.
Callbacks: the workhorse you already know
Callbacks are the default. In Node you get error first callbacks. In the browser you pass a function to XHR, to setTimeout, to an event. This is fine. The trouble starts when the number of moving parts grows. Missing error handling, multiple invocation, accidental re entry, and the classic pyramid of doom. Callbacks do not compose by themselves. Sequencing, parallel work, and error propagation take ceremony.
The fix is discipline. One callback per async thing. Always handle the error argument first in Node. Never call the same callback twice. Avoid early returns that skip the callback. Keep names honest so it reads like a story. If you need to run tasks in series or in parallel, use a helper like the async module in Node or a tiny utility that collects results and calls back once. It is not fancy, but it is predictable and fast.
One more thing. Callbacks push control flow away from you. That is fine as long as you kept a handle on cancellation, timeouts, and resource cleanup. If a request takes too long, bail. If a component unmounts or a page changes, clean up listeners. Leaks come from forgotten callbacks that still hold references to big objects.
Promises: the new contract
Promises are making a real entrance this year. The Promises A plus spec gives us a clear contract for async values that either resolve or reject. You can chain work with then, handle errors in one place, and compose multiple async pieces with simple helpers. Libraries like Q, when, and RSVP are small and ready for production. They run in the browser and in Node. They turn nested callbacks into a sequence that reads top to bottom.
Why it matters. With promises you get automatic error propagation. Throw inside a handler and the next catch sees it. Return a promise from a then and the chain waits. You can create a promise from a Node style function and now you have the same shape everywhere. This also makes testing easier because a test can wait on a single promise and assert once.
A note on jQuery Deferred. jQuery has a Deferred object that looks like a promise and it is close. It is not fully A plus. Some behaviors differ, especially around assimilation and timing. You can still use it in a jQuery codebase, just do not mix it with A plus promises without care. If you plan to bet on promises across your stack, pick one of Q, when, or RSVP and wrap your async sources consistently.
Patterns that keep code sane
Most async stories reduce to a few patterns. Learn them and your code stays readable.
Series: Do A then B then C. With callbacks you pass the next step into the current step. With promises you chain then calls. Keep each step a small function and share as little state as you can.
Parallel: Do A and B at the same time then continue. With callbacks you count completions. With promises you wait on all. Watch error handling here. Decide if one failure cancels the rest or if you collect partial results.
Race: Fire multiple options and take the first that finishes. Great for fallback CDNs or duplicate queries with a timeout guard. Make sure you cancel the losers so they do not cause side effects later.
Timeout, retry, backoff: Networks stall. Add a timeout to every call that can hang. If a request fails, retry with a simple exponential backoff and a cap. Do not hammer a service. Also log the final failure with enough context to debugg it later.
Cache and memo: If the cost of a call is high and the result is stable for a bit, cache it. In the browser this keeps the UI snappy. In Node it reduces load on downstream services. In both places it prevents duplicate work when multiple parts of the app ask for the same thing at almost the same time.
Callbacks vs promises vs events and streams
This is not a fight. Pick the tool that matches the shape of the work. Use callbacks when the API is Node style and performance is tight or when you are deep inside a hot path. Use promises when you need clear sequencing, shared error handling, and easier composition. Keep events for many to one signals like UI interactions and sockets. Keep streams for chunked data in Node where you can process while it arrives. They play well together. Wrap a callback API with a promise for the edges. Convert a readable stream into promises for the few steps that need order, then go back to streaming.
Practical checklist
- Keep error first callbacks in Node and always handle the error path.
- Never call the same callback twice. Guard with a flag if needed.
- Add a timeout to every network or disk call. Fail fast and surface the reason.
- Pick one promise library for your app. Q, when, or RSVP are good picks right now.
- Wrap callback APIs at the edge so the inside of your app uses one async shape.
- Do not mix jQuery Deferred with A plus promises unless you know the differences.
- Use series and parallel helpers instead of building them from scratch every time.
- Log unhandled rejections and thrown errors in async handlers. Make noise early.
- Cancel work that no longer matters. Clear timers and detach listeners on teardown.
- Keep functions small and name them well so async reads in a straight line.
- Do not block the event loop. Offload heavy work to a worker or a separate process.
One promise or one callback per unit of work. That single rule keeps surprises away.
Async is where bugs hide. Make it boring.