Программа JavaScript является однопоточной, и весь код выполняется последовательно, а не параллельно. Пример идет здесь

const second = () => {
    console.log('Second');
  }
const first = () => {
    console.log('First');
    second();
    console.log('The End');
  }
first();
//Results
First
Second
The End

В синхронном режиме все инструкции выполняются одна за другой, строка за строкой, только в том порядке, в котором они появляются в коде.

Обратные вызовы:
Теперь давайте добавим сюда асинхронный код. мы собираемся использовать функцию setTimeout, которая в основном предназначена для установки таймера в JavaScript.

const second = () => {
   setTimeout(() => {
      console.log('Second Async');
   }, 2000);
}
const first = () => {
   console.log('First');
   second();
   console.log('The End');
}
first();
//Results
First
The End
Second Async

Теперь, когда вызывается первая функция, в консоль записывается «Первая», а затем вызывается вторая функция. Теперь эта функция вызывает функцию Set Timeout, которая в основном похожа на таймер, который выполнит переданную нам функцию обратного вызова через 2000 миллисекунд. Однако это не остановит код на две секунды, а вместо этого функция вернется, вернется к первой функции и записывает «Конец». Затем, по прошествии двух секунд, «Second Async» регистрируется в консоли. Мы не ждем, пока функция закончит свою работу, а потом что-то делаем с результатом. Вместо этого мы позволяем этой функции выполнять свою работу в фоновом режиме, чтобы мы могли продолжить выполнение кода. Затем мы также передаем функцию обратного вызова, которая будет вызываться, как только основная функция сделает все, что ей нужно.

Ад обратного вызова:

Проблема с обратными вызовами заключается в том, что они создают ад обратных вызовов.
Мы начнем вкладывать функции в функции, и становится очень трудно читать код. Вот пример ада обратного вызова.

function getData() {
    setTimeout(() => {
      getData1();//Async Call.
      setTimeout(() => {
        getData2();//Async Call. 
        setTimeout(() => {
          getData3();
          console.log("Callback Hell");
             }, 1500);
        }, 1500);
    }, 1500);
}
getData();

Обещания:

В ES6 было введено нечто, называемое «обещаниями». А с помощью обещаний мы можем избежать всего этого ада обратных вызовов и получить более приятный и чистый синтаксис
при использовании асинхронного JavaScript. Обещание - это объект, который отслеживает, произошло ли определенное событие уже или нет, и если оно произошло, то обещание определяет, что произойдет дальше. И под событиями здесь я имею в виду асинхронное событие, такое как завершение работы таймера или данные, возвращаемые из вызова ajax. Обещание может иметь разные состояния. До того, как событие произошло, обещание отложено. Затем после того, как событие произошло, обещание называется урегулированным или разрешенным. Теперь, когда обещание было действительно успешным, что означает, что результат доступен, обещание выполнено, но если произошла ошибка, обещание отклоняется.

function onSuccess () {
  console.log('Success!')
}
function onError () {
  console.log('Fail')
}
const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve()
  }, 2000)
})
p.then(onSuccess).catch(onError)

И .then, и .catch вернут новое обещание., Что означает, что обещания могут быть связаны. Чтобы вы могли увидеть еще один пример, вот типичный вариант использования fetch API. fetch вернет вам обещание, которое разрешится с помощью ответа HTTP. Чтобы получить фактический JSON, вам нужно позвонить .json. Благодаря цепочке мы можем думать об этом последовательно.

fetch('/api/user.json')   
  .then((response) => response.json())   
  .then((user) => {     
     // user is now ready to go.  
 })

Асинхронный / Ожидание:

Теперь мы знаем, как строить и потреблять обещания. Но этот синтаксис для использования обещаний по-прежнему может быть довольно запутанным и сложным в управлении.
В ES8 или ES2017 в язык JavaScript было введено что-то под названием Async / Await, чтобы упростить нам, разработчикам < br /> потреблять обещания.

async function getPromise(){
   const data = await getData();
}
getPromise()

Если функция async возвращает значение, это значение также будет заключено в обещание. Это означает, что для доступа к нему вам нужно будет использовать .then.

async function add (x, y) {  
      return x + y 
}  
add(2,3)
.then((result) => {
   console.log(result) // 5 
})

С помощью Async / Await наиболее распространенный подход к обработке ошибок - заключить ваш код в блок try/catch.

try{
  async function add (x, y) {  
      return x + y 
  }  
  add(2,3)
   .then((result) => {
     console.log(result) // 5 
   })
}catch(e){
   console.log(e)
}

Приятного чтения!