JavaScript é de uma única thread
O JavaScript executa uma coisa de cada vez. Então, como ele baixa um arquivo ou espera uma resposta do servidor sem "congelar"? Graças ao event loop.
As operações que demoram (requisições de rede, temporizadores...) são delegadas ao ambiente (o navegador ou o Node) e o JavaScript continua executando o resto do código. Quando a tarefa termina, sua função de resposta é colocada em uma fila, e o event loop a executa assim que a thread principal estiver livre.
console.log("1");
setTimeout(() => console.log("2"), 0); // é adiado
console.log("3");
// Imprime: 1, 3, 2
Mesmo que o atraso seja 0, o callback do setTimeout espera o código
atual terminar. Essa é a chave da assincronia.
Callbacks
Historicamente, as tarefas assíncronas eram tratadas com callbacks: funções que você passa para serem chamadas "quando terminar".
baixar("dados.json", function (resultado) {
console.log("Pronto:", resultado);
});
O problema: aninhar muitos callbacks produz o temido callback hell, código em forma de pirâmide difícil de ler. As promessas nasceram para resolver isso.
Exemplos
A ordem não é a que parece
console.log("Início");
setTimeout(() => console.log("Tarefa adiada"), 0);
console.log("Fim");