- История развития асинхронного JavaScript
- Первые шаги: использование обратных вызовов
- Преимущества и недостатки колбэков
- Типичные ошибки при работе с колбэками
- Проблема вложенности
- Отсутствие обработки ошибок
- Проблемы с параллелизмом
- Отсутствие читабельности кода
- Заключение
- Переход к промисам
- Основные концепции и синтаксис промисов
- Что такое промис?
- Создание и использование промисов
- Цепочка промисов
- Практическое применение промисов
- Обработка ошибок с промисами
- Видео:
- Урок 5. JavaScript. Promise. Что это, как работает (+ пример)
История развития асинхронного JavaScript
На заре появления языка JavaScript, для обработки асинхронных задач использовались функции обратного вызова, или коллбэки. Этот метод был прост и понятен: программисту нужно было передать функцию в качестве аргумента, которая будет вызвана при наступлении определенного события. Например, загрузка изображения могла быть обработана с помощью коллбэка следующим образом:
function loadImage(imagesrc, callback) {
const img = new Image();
img.onload = function() {
callback(null, img);
};
img.onerror = function() {
callback(new Error('Ошибка загрузки изображения.'));
};
img.src = imagesrc;
}
Однако такой подход имел свои недостатки. Когда нужно было выполнить несколько асинхронных операций последовательно, приходилось вкладывать коллбэки друг в друга, что приводило к созданию «адской пирамиды коллбэков». Пример подобного кода:
doSomething(function(result1) {
doSomethingElse(result1, function(result2) {
thenupdateotherthing(result2, function(result3) {
// Продолжаем цепочку...
});
});
});
С появлением обещаний, или промисов, ситуация значительно улучшилась. Промисы позволили писать код, который легче читать и поддерживать. Пример использования промисов:
loadImage(imagesrc)
.then(function(parsed1) {
return processImage(parsed1);
})
.then(function(userresponse) {
return saveImage(userresponse);
})
.catch(function(error) {
console.error('Ошибка:', error);
});
Промисы предоставили возможность связывать асинхронные операции цепочкой вызовов методов then и catch, избавляя от вложенности коллбэков и делая код более линейным и понятным.
В настоящем, с появлением синтаксиса async/await, написание асинхронного кода стало ещё проще. Теперь мы можем писать асинхронные функции так, как будто они синхронные, что значительно улучшает читаемость и поддержку кода:
async function loadImageAndProcess(imagesrc) {
try {
const parsed1 = await loadImage(imagesrc);
const userresponse = await processImage(parsed1);
await saveImage(userresponse);
} catch (error) {
console.error('Ошибка:', error);
}
}
Этот синтаксис позволил избавиться от цепочек промисов, предоставляя разработчикам возможность писать асинхронный код в привычном императивном стиле. Таким образом, путь развития асинхронного программирования в JavaScript привел к созданию инструментов, которыми легко и приятно пользоваться.
Первые шаги: использование обратных вызовов
В начале развития современных веб-технологий разработчики столкнулись с задачей выполнения асинхронных операций. Для решения этой задачи стали применяться специальные функции, которые позволяли выполнять действия после завершения определённых операций. Эти функции получили широкое распространение и легли в основу многих библиотек и фреймворков.
Основная идея заключалась в том, чтобы передавать в функцию другой кусок кода, который выполнится по завершении операции. Такой подход обеспечивал необходимую гибкость и позволял обрабатывать результаты различных действий, не блокируя основной поток выполнения программы.
Рассмотрим простой пример, в котором функция fetchData1 запрашивает данные, а по завершению вызывает обработчик parsed1:
function fetchData1(callback) {
// имитация асинхронной операции
setTimeout(function() {
const data = { value: 'some data' };
callback(data);
}, 1000);
}
function parsed1(data) {
console.log('Данные получены:', data);
}
fetchData1(parsed1);
Такой подход имеет свои преимущества и недостатки. С одной стороны, он позволяет выполнить код после завершения операции, с другой — может привести к сложной структуре, известной как «ад коллбэков» (callback hell). Рассмотрим пример более сложной цепочки вызовов:
function fetchData1(callback) {
setTimeout(function() {
const data = { value: 'some data' };
callback(null, data);
}, 1000);
}
function thenUpdateOtherThing(err, data, callback) {
if (err) {
console.error(err);
return;
}
// обновление данных
setTimeout(function() {
const updatedData = { ...data, updated: true };
callback(null, updatedData);
}, 1000);
}
function displayImage(err, data) {
if (err) {
console.error(err);
return;
}
console.log('Отображение данных:', data);
}
fetchData1(function(err, data) {
if (err) {
console.error(err);
return;
}
thenUpdateOtherThing(err, data, displayImage);
});
Этот пример показывает, как легко можно запутаться в цепочке коллбэков, особенно при наличии нескольких уровней вложенности. Проблема сопровождаемости кода становится очевидной, когда увеличивается количество операций и их сложность.
В таких ситуациях на помощь приходят функции-генераторы и промисы, которые значительно упрощают реализацию асинхронного программирования. Тем не менее, понимание основ использования обратных вызовов является важным шагом на пути к освоению более сложных конструкций и подходов.
| Метод | Описание |
|---|---|
setTimeout | Выполняет функцию через определённое время |
callback | Функция, передаваемая в другую функцию для выполнения по завершении операции |
Таким образом, начав с простых обратных вызовов, можно постепенно переходить к более современным и мощным инструментам, таким как промисы и async/await, что значительно улучшит читабельность и управляемость вашего кода.
Преимущества и недостатки колбэков
Преимущества колбэков заключаются в их простоте и гибкости. Они позволяют объявлять функции, которые будут вызваны при завершении определенной задачи, будь то запрос к серверу или обработка данных. Колбэки облегчают выполнение последовательных асинхронных операций, так как каждая следующая задача может быть выполнена внутри предыдущего колбэка. Это позволяет лучше контролировать порядок выполнения кода и его структуру.
Кроме того, колбэки являются основой для более мощных инструментов, таких как промисы и async/await, которые значительно облегчают работу с асинхронным кодом. Например, промисы предоставляют удобные методы then и catch для обработки успешного или неудачного завершения операции, что упрощает чтение и поддержку кода. Современные инструменты, такие как Babel, могут трансформировать код с использованием async/await в колбэки, обеспечивая совместимость со старыми версиями JavaScript.
Однако у колбэков есть и недостатки. Основным из них является «callback hell» – ситуация, когда колбэки вложены друг в друга слишком глубоко, что делает код сложным для понимания и поддержки. Это может привести к ошибкам и затруднить выполнение задач. Кроме того, колбэки не всегда обеспечивают удобный способ обработки ошибок. В случае error-first колбэков, таких как function(err, result), разработчику приходится вручную обрабатывать ошибки в каждом вызове, что увеличивает вероятность пропустить важную ошибку.
Еще одним недостатком является сложность масштабирования и тестирования кода с большим количеством колбэков. Асинхронные операции могут зависеть друг от друга, и любая ошибка или задержка в одной из них может повлиять на всю цепочку. Это делает код менее надежным и увеличивает время на его отладку и тестирование.
Таким образом, несмотря на свою простоту и гибкость, колбэки имеют ряд недостатков, которые необходимо учитывать при разработке. Применение промисов и async/await может значительно упростить работу с асинхронными операциями и улучшить качество кода.
Типичные ошибки при работе с колбэками

Проблема вложенности
Когда колбэки начинают вызываться друг за другом, возникает так называемое «ад колбэков» или «callback hell». Это приводит к тому, что код становится сложно читать и поддерживать.
Пример кода:
function asyncFunc(callback) {
somethingsavefunction(err => {
if (err) {
return callback(err);
}
thenupdateotherthing(callback);
});
}
Решение проблемы вложенности — это использование функций-генераторов и промисов.
Отсутствие обработки ошибок
Зачастую разработчики забывают обрабатывать ошибки в колбэках, что приводит к непредсказуемому поведению программы. Важно всегда учитывать возможность возникновения ошибок и соответствующим образом их обрабатывать.
Пример кода:
function asyncFunc(callback) {
somethingsavefunction(err => {
if (err) {
// Обработка ошибки
console.error('Ошибка:', err);
return callback(err);
}
thenupdateotherthing(callback);
});
}
Проблемы с параллелизмом
При выполнении нескольких асинхронных операций параллельно колбэки могут вызвать трудности в синхронизации и управлении потоками данных. Это часто приводит к неуправляемому коду.
Пример кода:
function asyncFunc(callback) {
somethingsavefunction(err => {
if (err) {
return callback(err);
}
thenupdateotherthing(result => {
if (result.error) {
return callback(result.error);
}
callback(null, result);
});
});
}
Для решения таких проблем часто используют промисы, async-функции и другие механизмы управления параллелизмом.
Отсутствие читабельности кода
Использование большого количества колбэков приводит к ухудшению читабельности и сопровождаемости кода. Код с множеством вложенных колбэков становится трудным для понимания и изменения.
Пример кода:
function asyncFunc(callback) {
somethingsavefunction(err => {
if (err) {
return callback(err);
}
thenupdateotherthing(result => {
if (result.error) {
return callback(result.error);
}
callback(null, result);
});
});
}
Заключение
Использование колбэков имеет свои особенности и потенциальные проблемы, но знание типичных ошибок и способов их предотвращения поможет сделать ваш код более надежным и читабельным. Применение современных подходов, таких как промисы и async-функции, значительно упрощает работу с асинхронным кодом и делает его более управляемым.
| Тип ошибки | Описание | Решение |
|---|---|---|
| Вложенность | Многократные вызовы колбэков внутри друг друга | Использование промисов и async-функций |
| Отсутствие обработки ошибок | Пренебрежение обработкой ошибок в колбэках | Всегда обрабатывать ошибки |
| Проблемы с параллелизмом | Трудности в синхронизации асинхронных операций | Использование промисов и async-функций |
| Читабельность | Код с множеством вложенных колбэков сложно читать и поддерживать | Использование промисов и async-функций |
Переход к промисам
Переход к новому способу обработки асинхронных задач стал важным этапом в развитии программирования. Промисы значительно упростили работу с асинхронными операциями, сделали код более понятным и улучшили сопровождаемость проектов. В этой части статьи мы рассмотрим, как промисы помогают решить задачи, которые ранее были сложными для реализации с помощью колбэк-функций.
Словом, промисы предлагают новый интерфейс для работы с асинхронными вызовами. В отличие от классических коллбэков, они позволяют писать код последовательно, делая его более линейным и простым для чтения. Когда задача завершается, промис может быть либо успешно выполнен (resolved), либо отклонен из-за ошибки. Это позволяет легко управлять выполнением операций и обрабатывать ошибки централизованно.
Пример использования промисов выглядит следующим образом:
function savetothedb(value) {
return new Promise((resolve, reject) => {
// симуляция асинхронной задачи
setTimeout(() => {
if (value) {
resolve('Успех: данные сохранены');
} else {
reject('Ошибка: значение не определено');
}
}, 1000);
});
}
savetothedb('Мои данные')
.then(result => {
console.log(result);
})
.catch(error => {
console.error(error);
});
Промисы делают код более читаемым и управляемым. Если сравнивать с использованием традиционных коллбэков, где вам понадобилось бы писать вложенные функции для обработки результатов и ошибок, промисы позволяют это делать последовательно, используя методы .then() и .catch().
Еще один важный аспект использования промисов – это возможность выполнять несколько асинхронных операций параллельно и ожидать их завершения. Например, вы можете использовать метод Promise.all(), чтобы дождаться выполнения всех промисов:
Promise.all([promise1, promise2, promise3])
.then(results => {
console.log(results); // массив результатов
})
.catch(error => {
console.error(error);
});
Промисы позволяют избежать проблемы callback hell, когда код становится слишком вложенным и трудным для понимания. С помощью промисов вы можете писать код, который легче поддерживать и сопровождать.
Для демонстрации основных возможностей промисов, давайте рассмотрим следующий пример. У нас есть две асинхронные задачи: чтение данных из базы данных и их сохранение. Промисы помогут нам выполнить эти задачи последовательно, обработав возможные ошибки:
function readdb() {
return new Promise((resolve, reject) => {
// симуляция чтения данных
setTimeout(() => {
const data = 'Некоторые данные';
if (data) {
resolve(data);
} else {
reject('Ошибка: данные не найдены');
}
}, 1000);
});
}
function savetodb(data) {
return new Promise((resolve, reject) => {
// симуляция сохранения данных
setTimeout(() => {
if (data) {
resolve('Успех: данные сохранены');
} else {
reject('Ошибка: данные пусты');
}
}, 1000);
});
}
readdb()
.then(data => {
console.log('Данные прочитаны:', data);
return savetodb(data);
})
.then(result => {
console.log(result);
})
.catch(error => {
console.error(error);
});
Этот код выполняет чтение данных из базы данных, а затем сохраняет их, если чтение прошло успешно. В случае ошибки на любом этапе, она будет обработана в блоке .catch(), что значительно упрощает управление ошибками и делает код более чистым.
| Функция | Описание |
|---|---|
.then() | Обрабатывает успешное выполнение промиса |
.catch() | Обрабатывает ошибки промиса |
Promise.all() | Ожидает выполнение всех переданных промисов |
Использование промисов значительно улучшает код, делая его более читабельным, логичным и сопровождаемым. В общем, переход к промисам – это шаг вперед в развитии современного программирования.
Основные концепции и синтаксис промисов
Что такое промис?
Промис является объектом, который представляет собой текущее состояние и результат выполнения асинхронной операции. В общем случае промис может находиться в одном из трех состояний:
- Ожидание (pending) – начальное состояние, операция ещё не завершена.
- Выполнен (fulfilled) – операция успешно завершена, и у нас есть результат.
- Отклонён (rejected) – операция завершена с ошибкой.
Создание и использование промисов

Для создания промиса используется конструктор Promise, который принимает функцию-исполнитель с двумя аргументами: resolve и reject. Эти аргументы сами являются функциями, вызываемыми для изменения состояния промиса.
const myPromise = new Promise((resolve, reject) => {
let condition = true; // Условие для примера
if (condition) {
resolve("Промис выполнен успешно");
} else {
reject("Произошла ошибка");
}
});
Для обработки результата промиса используют методы then и catch. Метод then вызывается при успешном выполнении промиса, а catch – при его отклонении.
myPromise
.then(result => {
console.log(result); // "Промис выполнен успешно"
})
.catch(error => {
console.error(error); // "Произошла ошибка"
});
Цепочка промисов

Одним из ключевых преимуществ промисов является возможность их цепочечного использования. Это значит, что можно создавать последовательность промисов, где каждый последующий выполняется после успешного завершения предыдущего.
myPromise
.then(result => {
console.log(result);
return new Promise((resolve, reject) => {
resolve("Второй промис выполнен");
});
})
.then(result => {
console.log(result); // "Второй промис выполнен"
})
.catch(error => {
console.error(error);
});
Практическое применение промисов

Рассмотрим простой пример использования промисов для загрузки данных с сервера. Мы создадим функцию, которая возвращает промис, имитирующий загрузку данных.
function loadData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Данные загружены успешно");
}, 2000);
});
}
loadData()
.then(data => {
console.log(data); // "Данные загружены успешно"
})
.catch(error => {
console.error(error);
});
Таким образом, промисы предоставляют удобный интерфейс для работы с асинхронными операциями, улучшая читаемость и структурированность кода.
Обработка ошибок с промисами
При работе с промисами мы зачастую сталкиваемся с двумя основными способами обработки ошибок: использование then и catch. Первый способ включает обработку ошибок в цепочке вызовов then, где ошибка передается в следующий блок then до тех пор, пока не будет обработана. Второй способ более универсален и позволяет отдельно обрабатывать ошибки с помощью catch.
Рассмотрим пример, где функция fetchData загружает данные и возвращает промис. В случае успешного выполнения мы можем обработать результат, а в случае ошибки – перехватить её и вывести сообщение об ошибке.
fetchData()
.then(response => response.json())
.then(data => {
console.log('Data received:', data);
})
.catch(err => {
console.error('Error occurred:', err);
});
Таким образом, если в цепочке вызовов промиса возникает ошибка, она будет передана в блок catch, где её можно обработать и избежать неожиданного завершения программы. Это делает код более устойчивым и предсказуемым.
Однако, иногда требуется более сложная обработка ошибок, например, при использовании нескольких асинхронных операций. Рассмотрим пример, где мы сначала загружаем данные о пользователе, а затем сохраняем их в базу данных:
fetchData('/user/1')
.then(userResponse => userResponse.json())
.then(userData => {
return saveToTheDB(userData);
})
.then(saveResult => {
console.log('User data saved:', saveResult);
})
.catch(err => {
console.error('Error during fetching or saving data:', err);
});
В этом примере, если ошибка возникнет на любом этапе, будь то загрузка данных или их сохранение, она будет передана в блок catch и обработана соответствующим образом.
Промисы являются мощным инструментом для управления асинхронными операциями, и правильная обработка ошибок играет ключевую роль в создании надежного кода. Использование then и catch в цепочке промисов позволяет гибко и эффективно реагировать на различные ситуации, обеспечивая стабильность и предсказуемость работы приложений.








