how does asynchronous actually work under the hood..?

It all depends on the implementation my friend, every browser can do whatever they want under the hood if they follow the language specifications.

You don't have to worry about threads and all that stuff, but if you do, keep this in mind:

JavaScript is not a "threaded" language, it works with an event loop flow, in which an event is fired, and consecutive functions are fired after that, until there is nothing more to call. This is the reason why it's pretty hard to block the UI in JavaScript if you're writing "good" code.

Multiple functions can be called at the same time without blocking at all, that's the beauty of it. Each execution of a function has it's own lifetime, if 3 event handlers are fired at the same time, the 3 event handlers will run at the same time, and not on a linear execution.

A good example on how this works, and the diferences betwen event loops and classic threading, is node.js, i'll give you a example:

Supose you're listening for a request on a server, and 2 seconds after the request arrives you'll send a message. Now let's supose you duplicate that listener, and both listeners do the same thing. If you request the server, you'll get the two messages at the same time, 2 seconds after the request is made, instead of one message on 2 seconds, and the other one on 4 seconds. That means both listeners are runing at the same time, instead of following a linear execution as most systems do.

Async means: You tell some service (DOM, server, etc.) to perform an action, and you attach an event handler that will be executed once the service tells you, i've got what you want, or i've done what you needed. And that handler is executed as any other mouseclick or keypress is executed. The chaining of event handlers can be PAINFULL, but i believe it's waaay better than blocking the UI.

I hope you find this usefull, instead of more confusing.


This answer might come a bit late, but I also wondered how an async function might be implemented (in Node.js) and how the async function manages to return immediately before the provided callback gets executed.

I found this page about the Event Loop and process.nextTick() helpful to understand how it works in Node.js.

This example is taken from there:

let bar;

function someAsyncApiCall(callback) {
  process.nextTick(callback);
}

someAsyncApiCall(() => {
  console.log('bar', bar); // 1
});

bar = 1;

It shows that by placing the callback in a process.nextTick(), the script runs to completion, allowing all the variables, functions, etc., to be initialised prior to the callback being called.

Here is another example by me:

// defining an asynchronous function
var async_function = function(callback){
    console.log("Executing code inside the async function");
    process.nextTick(function(){
      console.log("Executing code in callback provided to process.nextTick() inside async function");

      const startCallback = Date.now();
      while (Date.now() - startCallback < 2000) {
        // do nothing
      }
      callback();
    });
};

console.log("Code executed prior to calling the async function");
// calling the async function
async_function(function() {
    console.log("Executing the callback function provided to async function");
});
console.log("Code executed after calling the async function");

// Output:

// Code executed prior to calling the async function
// Executing code inside the async function
// Code executed after calling the async function
// Executing code in callback provided to process.nextTick() inside async function
// (pause of 2 seconds)
// Executing the callback function provided to async function

In quite a few asynchronous environments, you have just one thread working away on stuff. Let's call that stuff the 'foreground'. The stuff that gets worked on is scheduled, by some sort of scheduler. A queue of function pointers probably forms the guts of that scheduler. Function pointers are pretty much the same as callbacks.

When the foreground wants to do something time-consuming, e.g. query a database, or read a file, then the foreground makes the request to the underlying OS or library, and leaves a callback to be called when that time-consuming thing is done. (That processing goes on in another thread, possibly in another process, or in the kernel, which is ever so asynchronous.) The request has some sort of ID associated with it, so that when the task is done, the correct call back gets invoked with the correct results.

While that request is going on, the foreground can continue execute whatever comes next. And if a block of execution is done, the foreground will return to the scheduler. The scheduler will pick the next task from the queue. And one of those tasks will be to run some callback function, passing in the right data, for some slow operation that's just finished.