Sincronicidade no Javascript

JavaScript é uma linguagem síncrona

Em sua forma mais básica, o JavaScript é uma linguagem síncrona, de bloqueio e de thread único, na qual apenas uma operação pode estar em andamento por vez.

Mas os navegadores web definem funções e APIs que nos permitem registrar funções que não devem ser executadas de forma síncrona, e sim invocadas de forma assíncrona quando ocorre algum tipo de evento (a passagem do tempo, a interação do usuário com o mouse ou a chegada de dados pela rede, por exemplo).

Isso significa que você pode deixar seu código fazer várias coisas ao mesmo tempo sem parar ou bloquear seu thread principal.

Se queremos executar o código de forma síncrona ou assíncrona, dependerá do que estamos tentando fazer.

Há momentos em que queremos que as coisas carreguem e aconteçam imediatamente. Por exemplo, ao aplicar alguns estilos definidos pelo usuário a uma página da Web, você desejará que os estilos sejam aplicados o mais rápido possível.

No entanto, se estivermos executando uma operação que leva tempo, como consultar um banco de dados e usar os resultados para preencher modelos, é melhor enviar isso para fora do encadeamento principal e concluir a tarefa de forma assíncrona. Com o tempo, você aprenderá quando faz mais sentido escolher uma técnica assíncrona em vez de uma síncrona.

JavaScript é single threaded

JavaScript é single-threaded, ver conceito de Thread.
Mesmo com múltiplos núcleos de processamento, você só pode fazê-lo executar tarefas em uma única thread, chamada de main thread (thread principal).

Web workers

Depois de um tempo, o JavaScript ganhou algumas ferramentas para ajudar em tais problemas. As Web workers te permitem mandar parte do processamento do JavaScript para uma thread separada. Você geralmente usaria uma worker para executar um processo pesado para que a UI não seja bloqueada.

                        Main thread:   Tarefa A --> Tarefa C
                        Worker thread: Tarefa pesada B
                    
Podem ser bem úteis, mas elas tem as suas limitações.

A primeira limitação é que elas não são capazes de acessar a DOM — você não pode fazer com que uma worker faça algo diretamente para atualizar a UI, basicamente ela pode apenas fazer cálculos de números.

A segunda limitação é que, mesmo que o código executado em uma worker não cause um bloqueio, ele ainda é um código síncrono. Isso se torna um problema quando uma função depende dos resultados de processos anteriores para funcionar.

PROBLEMAS, Considere os diagramas a seguir:

                            Main thread: Tarefa A --> Tarefa B
                        
Nesse caso, digamos que a tarefa A está fazendo algo como pegar uma imagem do servidor e que a tarefa B faz algo com essa imagem, como colocar um filtro nela.
Se iniciar a tarefa A e depois tentar executar a tarefa B imediatamente, ocorrerá um erro, porque a imagem não estará disponível ainda.


                            Main thread:   Tarefa A --> Tarefa B --> |Tarefa D|
                            Worker thread: Tarefa C ---------------> |        |
                        
Neste caso, digamos que a tarefa D faz uso dos resultados das tarefas B e C.
Se nós pudermos garantir que esses resultados estejam disponíveis ao mesmo tempo, então tudo talvez esteja bem, mas isso não é garantido.
Se a tarefa D tentar ser executada quando um dos resultados não estiver disponível, retornará um erro.
SOLUÇÃO
Para consertarmos tais problemas, os browsers nos permitem executar certas operações de modo assíncrono. Recursos como Promises te permitem executar uma operação e depois esperar pelo resultado antes de executar outra operação:

                            Main thread: Tarefa A                      Tarefa B
                            Promise:             |___operação async___|
                        
Já que a operação está acontecendo em outro lugar, a main thread não está bloqueada enquanto a operação assíncrona está sendo processada.

Como podemos escrever código assíncrono em JavaScript?

References