Bài 7.1: Promises và cách sử dụng trong JavaScript

Bài 7.1: Promises và cách sử dụng trong JavaScript
Chào mừng bạn quay trở lại với chuỗi bài blog về Lập trình Web Front-end! Hôm nay, chúng ta sẽ đi sâu vào một khái niệm cực kỳ quan trọng trong JavaScript hiện đại, đặc biệt là khi làm việc với các tác vụ cần thời gian để hoàn thành: Promises.
Nếu bạn đã từng gặp khó khăn với các hàm callback lồng nhau chồng chéo tạo ra "callback hell", thì Promises chính là cứu tinh mà bạn đang tìm kiếm. Promises mang đến một cách tiếp cận có tổ chức và dễ quản lý hơn rất nhiều đối với mã bất đồng bộ (asynchronous code).
Bất Đồng Bộ Là Gì và Tại Sao Cần Promises?
Trong JavaScript, đặc biệt là trong môi trường trình duyệt hoặc Node.js, rất nhiều hoạt động diễn ra không đồng bộ (asynchronous). Điều này có nghĩa là chúng ta khởi tạo một tác vụ (ví dụ: tải dữ liệu từ mạng, đọc file, thiết lập bộ hẹn giờ) và sau đó không chờ nó hoàn thành ngay lập tức. Thay vào đó, chúng ta để chương trình tiếp tục chạy các tác vụ khác, và khi tác vụ bất đồng bộ kia hoàn thành, nó sẽ thông báo lại cho chúng ta biết kết quả.
Cách truyền thống để xử lý kết quả của tác vụ bất đồng bộ là sử dụng callback functions. Chúng ta truyền một hàm như một đối số, và hàm này sẽ được gọi khi tác vụ bất đồng bộ hoàn thành.
Tuy nhiên, khi có nhiều tác vụ bất đồng bộ phụ thuộc vào nhau hoặc cần thực hiện theo một trình tự nhất định, việc sử dụng callback lồng nhau có thể dẫn đến cấu trúc mã khó đọc, khó hiểu và khó bảo trì, hay còn gọi là "callback hell".
// Ví dụ callback hell (tưởng tượng)
getData(function(a) {
processData(a, function(b) {
saveData(b, function(c) {
console.log("Successfully processed and saved data:", c);
}, function(err) {
console.error("Save error:", err);
});
}, function(err) {
console.error("Process error:", err);
});
}, function(err) {
console.error("Get data error:", err);
});
Bạn thấy đấy, cấu trúc tam giác này nhanh chóng trở nên phức tạp. Đây chính là lúc Promises xuất hiện để giải quyết vấn đề này.
Promises Là Gì?
Một Promise trong JavaScript là một đối tượng đại diện cho kết quả cuối cùng của một thao tác bất đồng bộ -- kết quả này có thể có sẵn bây giờ, có thể có trong tương lai, hoặc không bao giờ có.
Hiểu đơn giản, Promise giống như một lời hứa:
- Nó hứa sẽ hoàn thành một việc gì đó trong tương lai.
- Khi hoàn thành, nó sẽ cho bạn biết kết quả: hoặc là thành công (fulfilled/resolved) và trả về một giá trị, hoặc là thất bại (rejected) và trả về lý do thất bại (một lỗi).
Một Promise luôn ở một trong ba trạng thái (state):
pending
: Trạng thái ban đầu, chưa hoàn thành cũng chưa thất bại.fulfilled
(hayresolved
): Thao tác đã hoàn thành thành công.rejected
: Thao tác đã thất bại.
Quan trọng là: một Promise chỉ có thể chuyển từ trạng thái pending
sang fulfilled
hoặc rejected
một lần duy nhất. Sau khi đã chuyển sang fulfilled
hoặc rejected
(gọi chung là settled
), trạng thái của Promise sẽ không bao giờ thay đổi nữa.
Tạo Một Promise
Bạn có thể tạo một Promise mới bằng cách sử dụng hàm tạo new Promise()
. Hàm tạo này nhận một hàm thực thi (executor function) làm đối số. Hàm thực thi này nhận hai đối số khác là resolve
và reject
(thường là tên gọi thông convention):
const myPromise = new Promise((resolve, reject) => {
// Bên trong đây là nơi thực hiện tác vụ bất đồng bộ
// Sau khi tác vụ hoàn thành:
// - Nếu thành công: gọi resolve(value)
// - Nếu thất bại: gọi reject(reason)
const success = true; // Giả định kết quả tác vụ
if (success) {
// Thành công, gọi resolve với giá trị kết quả
setTimeout(() => { // Mô phỏng tác vụ mất thời gian
resolve("Thao tác thành công!");
}, 1000); // Hoàn thành sau 1 giây
} else {
// Thất bại, gọi reject với lý do lỗi
setTimeout(() => { // Mô phỏng tác vụ mất thời gian
reject("Thao tác thất bại!");
}, 1000); // Thất bại sau 1 giây
}
});
- Hàm thực thi (
(resolve, reject) => { ... }
) chạy ngay lập tức khinew Promise
được tạo. resolve
: Hàm này được gọi khi tác vụ bất đồng bộ thành công. Bạn truyền giá trị kết quả vào hàmresolve
. Việc gọiresolve
làm cho Promise chuyển sang trạng tháifulfilled
.reject
: Hàm này được gọi khi tác vụ bất đồng bộ thất bại. Bạn truyền lý do lỗi (thường là một đối tượngError
) vào hàmreject
. Việc gọireject
làm cho Promise chuyển sang trạng tháirejected
.
Trong ví dụ trên, chúng ta sử dụng setTimeout
để mô phỏng một tác vụ bất đồng bộ mất 1 giây. Tùy thuộc vào giá trị của biến success
, Promise sẽ được resolve
hoặc reject
.
Sử Dụng (Consume) Một Promise
Sau khi có một Promise, làm thế nào để biết khi nào nó hoàn thành và xử lý kết quả? Chúng ta sử dụng các phương thức của Promise: .then()
, .catch()
, và .finally()
.
.then()
: Xử lý khi Promise thành công
Phương thức .then()
được sử dụng để đăng ký hàm callback sẽ được gọi khi Promise chuyển sang trạng thái fulfilled
. Nó nhận một hàm làm đối số, hàm này sẽ nhận giá trị được truyền vào resolve()
.
myPromise.then((result) => {
console.log("Promise resolved with:", result); // Output: "Promise resolved with: Thao tác thành công!"
});
.then()
được gọi trên đối tượngmyPromise
.- Nếu
myPromise
đượcresolve
, hàm callback(result) => { ... }
sẽ được thực thi vớiresult
là giá trị bạn đã truyền vàoresolve()
.
.then()
cũng có thể nhận đối số thứ hai là một hàm callback để xử lý khi Promise bị reject
. Tuy nhiên, cách này không được khuyến khích bằng việc sử dụng .catch()
.
.catch()
: Xử lý khi Promise thất bại
Phương thức .catch()
là cách chuẩn và rõ ràng nhất để xử lý khi Promise chuyển sang trạng thái rejected
. Nó là cách viết tắt của .then(null, onRejected)
. .catch()
nhận một hàm làm đối số, hàm này sẽ nhận lý do lỗi được truyền vào reject()
.
// Tạo một Promise bị reject để minh họa
const anotherPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("Có gì đó sai sai rồi!"));
}, 1000);
});
anotherPromise
.then(result => {
console.log("This won't be called:", result);
})
.catch(error => {
console.error("Promise rejected with error:", error); // Output: "Promise rejected with error: Error: Có gì đó sai sai rồi!"
});
- Nếu
anotherPromise
bịreject
, hàm callback trong.catch()
sẽ được thực thi vớierror
là lý do lỗi bạn đã truyền vàoreject()
. - Hàm callback trong
.then()
không được gọi trong trường hợp này.
.finally()
: Thực thi dù thành công hay thất bại
Phương thức .finally()
được sử dụng để đăng ký một hàm callback sẽ được thực thi khi Promise settled
(chuyển sang trạng thái fulfilled
hoặc rejected
). Nó hữu ích cho các tác vụ "dọn dẹp", chẳng hạn như ẩn một spinner loading, bất kể thao tác thành công hay thất bại.
const thirdPromise = new Promise((resolve, reject) => {
const success = Math.random() > 0.5; // Ngẫu nhiên thành công hoặc thất bại
setTimeout(() => {
if (success) {
resolve("Hoàn thành!");
} else {
reject("Thất bại!");
}
}, 1000);
});
thirdPromise
.then(result => {
console.log("Success handler:", result);
})
.catch(error => {
console.error("Error handler:", error);
})
.finally(() => {
console.log("Promise settled (finally handler)."); // Sẽ luôn chạy sau khi Promise hoàn thành
});
- Hàm callback trong
.finally()
không nhận bất kỳ đối số nào. - Nó luôn chạy sau khi
.then()
hoặc.catch()
(hoặc cả hai nếu có logic phức tạp hơn) đã hoàn thành.
Chuỗi Promise (Promise Chaining)
Một trong những lợi ích lớn nhất của Promises so với callback là khả năng xây dựng chuỗi (chaining) các tác vụ bất đồng bộ một cách dễ đọc và dễ quản lý. Điều này có được là nhờ vào việc phương thức .then()
(và .catch()
) luôn trả về một Promise mới.
Khi bạn trả về một giá trị từ hàm callback trong .then()
, giá trị đó sẽ trở thành kết quả của Promise mới được trả về bởi .then()
đó. Promise .then()
tiếp theo trong chuỗi sẽ nhận giá trị này làm đầu vào.
Khi bạn trả về một Promise khác từ hàm callback trong .then()
, Promise mới được trả về bởi .then()
đó sẽ chờ Promise bạn vừa trả về hoàn thành. Khi Promise đó hoàn thành (resolve hoặc reject), Promise tiếp theo trong chuỗi sẽ tiếp tục xử lý tương ứng.
Ví dụ về chuỗi Promise:
// Hàm mô phỏng tác vụ bất đồng bộ trả về Promise
function asyncTask(value) {
console.log("Executing task with value:", value);
return new Promise((resolve, reject) => {
setTimeout(() => {
const newValue = value * 2;
console.log("Task completed, new value:", newValue);
resolve(newValue); // Trả về giá trị cho Promise tiếp theo
}, 500);
});
}
// Bắt đầu chuỗi với một Promise đã được giải quyết (Promise.resolve) hoặc một Promise mới
Promise.resolve(5) // Bắt đầu với giá trị 5
.then(asyncTask) // .then sẽ nhận 5, gọi asyncTask(5), chờ asyncTask trả về Promise
.then(asyncTask) // .then này sẽ nhận kết quả từ asyncTask(5) (là 10), gọi asyncTask(10)
.then(finalResult => { // .then này sẽ nhận kết quả từ asyncTask(10) (là 20)
console.log("Final result:", finalResult); // Output: Final result: 20
})
.catch(error => {
console.error("An error occurred in the chain:", error);
});
// Output dự kiến theo thứ tự thời gian:
// Executing task with value: 5
// Task completed, new value: 10
// Executing task with value: 10
// Task completed, new value: 20
// Final result: 20
Promise.resolve(5)
tạo ra một Promise đã ở trạng tháifulfilled
với giá trị là 5..then(asyncTask)
nhận giá trị 5, gọi hàmasyncTask
với đối số 5. Hàm này trả về một Promise..then
đầu tiên chờ Promise này hoàn thành.- Khi Promise từ
asyncTask(5)
hoàn thành vàresolve(10)
,.then
thứ hai nhận giá trị 10, gọiasyncTask
với đối số 10. Hàm này trả về một Promise khác..then
thứ hai chờ Promise này hoàn thành. - Khi Promise từ
asyncTask(10)
hoàn thành vàresolve(20)
,.then
cuối cùng nhận giá trị 20 và in ra kết quả cuối cùng. - Nếu bất kỳ Promise nào trong chuỗi bị
reject
, chuỗi sẽ bỏ qua các.then()
còn lại và nhảy thẳng đến.catch()
gần nhất trong chuỗi.
Xử Lý Lỗi Trong Chuỗi Promise
Việc xử lý lỗi với .catch()
trong chuỗi Promise rất mạnh mẽ. Khi một Promise trong chuỗi bị rejected
, hoặc khi một lỗi (exception) được ném ra trong bất kỳ hàm callback .then()
nào, luồng xử lý sẽ ngay lập tức chuyển đến .catch()
gần nhất trong chuỗi.
Promise.resolve("Start")
.then(value => {
console.log(value); // Output: Start
return "First step completed";
})
.then(value => {
console.log(value); // Output: First step completed
throw new Error("Something went wrong in the second step!"); // Ném ra lỗi
return "Second step completed"; // Dòng này sẽ không chạy
})
.then(value => {
console.log(value); // Dòng này sẽ không chạy
return "Third step completed";
})
.catch(error => {
console.error("Caught an error:", error.message); // Output: Caught an error: Something went wrong in the second step!
// Có thể xử lý lỗi ở đây, hoặc ném lỗi mới để catch tiếp theo xử lý
// throw new Error("Handling failed too!");
})
.then(() => {
console.log("This .then runs after catch!"); // Output: This .then runs after catch! (nếu catch không ném lỗi mới)
})
.finally(() => {
console.log("Cleanup complete."); // Output: Cleanup complete.
});
- Lỗi ném ra trong
.then
thứ hai ngay lập tức làm cho Promise được trả về bởi.then
đó bịrejected
. - Luồng xử lý bỏ qua
.then
thứ ba và nhảy đến.catch()
. - Hàm callback trong
.catch()
được thực thi với đối tượng lỗi. - Nếu hàm callback trong
.catch()
không ném ra lỗi mới, Promise được trả về bởi.catch()
sẽ đượcresolved
(với giá trịundefined
nếu không trả về gì cụ thể), và các.then()
sau.catch()
vẫn sẽ chạy. Điều này cho phép bạn tiếp tục chuỗi xử lý sau khi đã "bắt" và xử lý lỗi.
Các Phương Thức Tiện Ích Của Promise
Đối tượng Promise
toàn cục cung cấp một số phương thức tĩnh hữu ích để làm việc với nhiều Promises cùng lúc:
Promise.all(iterable)
: Trả về một Promise mới. Promise này sẽ resolve khi tất cả các Promises trongiterable
(ví dụ: một mảng) đềuresolve
. Giá trị trả về là một mảng chứa kết quả của từng Promise theo đúng thứ tự. Nếu bất kỳ Promise nào trongiterable
bịreject
,Promise.all
sẽ ngay lập tức reject với lý do lỗi của Promise đầu tiên bịreject
.const p1 = Promise.resolve(3); const p2 = 1337; // Giá trị không phải Promise cũng được chấp nhận const p3 = new Promise((resolve, reject) => { setTimeout(() => resolve('foo'), 100); }); Promise.all([p1, p2, p3]) .then(values => { console.log(values); // Output: [3, 1337, "foo"] }) .catch(error => { console.error("One of the promises failed:", error); });
Promise.race(iterable)
: Trả về một Promise mới. Promise này sẽ settle (tức làresolve
hoặcreject
) ngay khi Promise đầu tiên trongiterable
settle. Kết quả hoặc lỗi sẽ là của Promise đầu tiên đó. Hữu ích khi bạn muốn thực hiện nhiều tác vụ và chỉ cần kết quả của tác vụ nào hoàn thành nhanh nhất.const p4 = new Promise((resolve, reject) => setTimeout(() => resolve('Quick'), 100)); const p5 = new Promise((resolve, reject) => setTimeout(() => reject('Slow Error'), 500)); const p6 = new Promise((resolve, reject) => setTimeout(() => resolve('Slower'), 800)); Promise.race([p4, p5, p6]) .then(result => { console.log(result); // Output: "Quick" (p4 resolve trước) }) .catch(error => { console.error(error); // Sẽ không chạy trong trường hợp này vì p4 resolve trước p5 reject });
Promise.any(iterable)
: Trả về một Promise mới. Promise này sẽ resolve ngay khi Promise đầu tiên trongiterable
resolve
. Nếu tất cả các Promises trongiterable
đều bịreject
,Promise.any
sẽ reject với mộtAggregateError
chứa tất cả các lý do lỗi.const p7 = new Promise((resolve, reject) => setTimeout(() => reject('Error A'), 200)); const p8 = new Promise((resolve, reject) => setTimeout(() => resolve('Success B'), 100)); const p9 = new Promise((resolve, reject) => setTimeout(() => resolve('Success C'), 300)); Promise.any([p7, p8, p9]) .then(result => { console.log(result); // Output: "Success B" (p8 resolve trước p9) }) .catch(error => { console.error(error); // Sẽ chỉ chạy nếu TẤT CẢ reject });
Promise.allSettled(iterable)
: Trả về một Promise mới. Promise này sẽ resolve khi tất cả các Promises trongiterable
đều đã settled (dù làfulfilled
hayrejected
). Giá trị trả về là một mảng các đối tượng, mỗi đối tượng mô tả kết quả của từng Promise (bao gồm trạng thái và giá trị/lý do).const p10 = Promise.resolve('Success!'); const p11 = Promise.reject('Failure!'); const p12 = new Promise(resolve => setTimeout(() => resolve('Delayed Success!'), 50)); Promise.allSettled([p10, p11, p12]) .then(results => { console.log(results); // Output: // [ // { status: 'fulfilled', value: 'Success!' }, // { status: 'rejected', reason: 'Failure!' }, // { status: 'fulfilled', value: 'Delayed Success!' } // ] });
Promise.allSettled
rất hữu ích khi bạn muốn biết kết quả của tất cả các tác vụ bất đồng bộ, ngay cả khi một số trong số chúng thất bại.
Promises và Async/Await
Bạn có thể đã nghe về async
và await
. Điều quan trọng cần nhớ là async/await
chỉ là cú pháp "đường" (syntactic sugar) được xây dựng dựa trên Promises. Mọi hàm async
đều trả về một Promise, và từ khóa await
chỉ có thể được sử dụng bên trong một hàm async
để tạm dừng việc thực thi cho đến khi một Promise được giải quyết.
Hiểu rõ Promises là nền tảng vững chắc để làm việc hiệu quả với async/await
, công cụ giúp code bất đồng bộ của bạn trở nên giống code đồng bộ, dễ đọc và dễ viết hơn nữa. Chúng ta sẽ tìm hiểu sâu hơn về async/await
trong các bài sau.
// Ví dụ fetch API với Promises
fetch('https://rickandmortyapi.com/api/character/1')
.then(response => {
if (!response.ok) { // Kiểm tra lỗi HTTP
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json(); // response.json() cũng trả về một Promise
})
.then(data => {
console.log("Character name:", data.name);
})
.catch(error => {
console.error("Fetch failed:", error);
});
// Ví dụ fetch API với async/await (tương đương với code trên)
async function fetchCharacter() {
try {
const response = await fetch('https://rickandmortyapi.com/api/character/1');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log("Character name (async/await):", data.name);
} catch (error) {
console.error("Fetch failed (async/await):", error);
}
}
fetchCharacter();
So sánh hai ví dụ trên, bạn có thể thấy code sử dụng async/await
trông gọn gàng và dễ hiểu hơn nhiều, đặc biệt là khi cần xử lý nhiều bước bất đồng bộ tuần tự. Tuy nhiên, cả hai đều dựa trên cơ chế Promise underlying.
Tại Sao Nên Sử Dụng Promises?
Tóm lại, Promises mang lại những lợi ích đáng kể so với việc chỉ sử dụng callback thuần túy:
- Tránh "Callback Hell": Cấu trúc chuỗi
.then()
giúp làm phẳng cấu trúc mã, dễ đọc và quản lý hơn. - Xử lý lỗi tập trung:
.catch()
cung cấp một cơ chế mạnh mẽ để xử lý lỗi cho toàn bộ chuỗi Promise. - Khả năng compose (tổ hợp): Các phương thức như
Promise.all()
,Promise.race()
,... giúp dễ dàng kết hợp nhiều tác vụ bất đồng bộ. - Trạng thái rõ ràng: Promise có các trạng thái được định nghĩa rõ ràng (
pending
,fulfilled
,rejected
) giúp theo dõi luồng xử lý. - Tương thích với
async/await
: Làm quen với Promises là bước đệm cần thiết để sử dụngasync/await
hiệu quả.
Comments