Чем отличаеться promise от обычных коллбэков?
Не то же самоеfunction ajax() {
  return fetch(target, fetchOptions)
    .then((response) => {
      if (!target) throw new Error('Invalid url');
      if (response.ok) return response.json();
      throw new Error(`${response.status} ${response.statusText}`);
    })
    .then((json) => {
        if (json['statusOk']) return json;
        throw new Error(json['message'] || 'Server response was not ok');
    });
}

ajax(action, { body: formData })
  .then((json) => {
    console.log('RESPONSE', json);
  })
  .catch((error) => {
    console.error(error);
  });
Мы можем писать сколько угодно зенов, в каждом что-то делать с данными.
Если в любом месте вылетит ошибка, она будет нормально обработана в кэтче. 
Код при этом линейный, без дикой вложенности.
С коллбэками вам нужно на каждом этапе отслеживать ошибки и возвращать их. Легко потеряться, что откуда всплыло или где пропало.
Альтернатива обработки ошибок — трай/кэтч. Но это не работает на асинхронном коде.function myFunc(cb){
  var err = true;
  // имитируем асинхронную операцию
  setTimeout(function(){
    cb(err);
  }, 10);
}

try {
  myFunc(function(err){
    if (err) throw new Error('Oops');
  });
  alert('Всё как бы хорошо!');
} catch(e) {
  alert(e.message);
}
Этот код выведет сообщение 'Всё как бы хорошо!', хотя всё плохо, была ошибка. https://jsfiddle.net/r3zfa4ee/
С промисами иначе:function myFunc(){
  let promise = new Promise((resolve, reject) => {
    let err = true;
  
    // имитируем асинхронную операцию
    setTimeout(() => {
      if (err) {
        reject('Ooops!');
      } else {
        resolve(123);
      }
    }, 10);
  });
  
  return promise;
}

myFunc()
  .then(data => {
    alert('Всё точно хорошо!');
  })
  .catch(e => {
    alert(e);
  });
Здесь уже ошибка будет обработана как надо и мы увидим сообщение 'Ooops!' https://jsfiddle.net/43yh8jad/
Мы можем выполнить промисы последовательно. Мы можем выполнить их параллельно. Мы можем запустить их на параллельное выполнение, но дождаться только первого отработавшего (так называемая promise race). Разумеется всё это можно и без промисов написать, но какая будет разница в объеме кода и в сложности его понимания?
А еще есть async/await...
Кучей всяких методов bluebirdjs.com/docs/api-reference.html
И ненужностью передавать ошибки первым аргументом