Long-running operations like network requests must run asynchronously so the browser stays responsive. JavaScript represents these operations with Promises, which are objects that describe an eventual completion or failure. A promise is in one of three states—pending, fulfilled, or rejected—and you attach handlers with .then() for success and .catch() for errors. Promise chaining sequences asynchronous steps by returning a value or another promise from each .then() callback, which keeps async workflows flat rather than deeply nested. A common mistake when chaining is forgetting to return a promise, which makes the next step run before the previous one has actually settled.
The async and await keywords build on top of promises and make asynchronous code look synchronous. An async function always returns a promise, and await pauses execution inside the function until the awaited promise settles, as in const data = await fetch(url);. Errors must be handled with try/catch around await calls, or by chaining .catch() on the promise. A frequent bug in async error handling is wrapping only the synchronous parts of a function and missing rejected promises, which can silently break user flows. Asking the questions—"what problem is this solving?", "what trade-off does it create?", and "how will I know it worked?"—is a useful habit before introducing async logic in any real project.
Under the hood, the JavaScript engine maintains a call stack, a LIFO record of which functions are currently running, and combines it with the event loop to schedule asynchronous tasks such as setTimeout callbacks. Within that loop, microtasks—including promise callbacks—run with higher priority than regular macrotasks, executing after the current synchronous stack finishes but before the next macrotask. Understanding this distinction helps explain why a promise's .then() handler fires sooner than a setTimeout(fn, 0), and it is the foundation for predicting the order of asynchronous operations in modern JavaScript.