Cách sử dụng Promises JavaScript

Javascript nâng cao | by Học Javascript

Trong JavaScript, xử lý bất đồng bộ là một phần quan trọng, đặc biệt khi làm việc với API, thao tác file, hoặc xử lý dữ liệu từ máy chủ. Trước khi có Promise, lập trình viên thường sử dụng callback, nhưng cách tiếp cận này dễ dẫn đến Callback Hell, khiến mã nguồn trở nên phức tạp và khó bảo trì.

Promise ra đời như một giải pháp giúp viết mã dễ đọc hơn, quản lý luồng bất đồng bộ hiệu quả hơn và hạn chế lỗi trong quá trình xử lý. Với khả năng chuỗi (.then()), bắt lỗi (.catch()) và hỗ trợ async/await, Promise đã trở thành công cụ không thể thiếu trong JavaScript hiện đại. Trong bài viết này, mình sẽ tìm hiểu cách sử dụng Promise, các phương thức quan trọng và cách tối ưu hóa khi làm việc với bất đồng bộ.

Promise trong JavaScript là gì?

Promise là một đối tượng trong JavaScript được sử dụng để xử lý các tác vụ bất đồng bộ. Nó đại diện cho một giá trị có thể chưa được xác định tại thời điểm hiện tại nhưng sẽ có kết quả trong tương lai. Promise giúp quản lý các thao tác bất đồng bộ một cách dễ đọc hơn và tránh tình trạng Callback Hell.

Các trạng thái của một Promise

Một Promise trong JavaScript có thể tồn tại ở một trong ba trạng thái sau:

  • pending (đang chờ xử lý): Đây là trạng thái ban đầu khi một Promise được tạo ra. Ở trạng thái này, Promise chưa có kết quả, và nó có thể chuyển sang fulfilled hoặc rejected.

  • fulfilled (đã hoàn thành): Khi một Promise hoàn tất thành công, nó chuyển sang trạng thái này và trả về giá trị mong đợi.

  • rejected (bị từ chối): Khi một Promise gặp lỗi hoặc thất bại, nó sẽ chuyển sang trạng thái này và trả về một lý do (error).

Khi một Promise đã chuyển sang trạng thái fulfilled hoặc rejected, nó không thể thay đổi trạng thái nữa.

Cách một Promise hoạt động trong JavaScript

Một Promise được tạo bằng cách sử dụng new Promise(executor), trong đó executor là một hàm có hai tham số:

  • resolve(value): Hàm này được gọi khi Promise hoàn thành thành công.

  • reject(error): Hàm này được gọi khi Promise thất bại hoặc xảy ra lỗi.

Dưới đây là cách một Promise hoạt động:

  • Khi Promise được tạo, nó bắt đầu ở trạng thái pending.

  • Nếu quá trình xử lý thành công, resolve(value) được gọi, chuyển trạng thái thành fulfilled.

  • Nếu có lỗi xảy ra, reject(error) được gọi, chuyển trạng thái thành rejected.

  • Khi một Promise hoàn thành (fulfilled hoặc rejected), nó kích hoạt các phương thức .then() hoặc .catch() để xử lý kết quả.

Ví dụ minh họa về Promise

Ví dụ sau minh họa cách tạo và sử dụng một Promise để mô phỏng việc lấy dữ liệu từ máy chủ:

// Tạo một Promise
const fetchData = new Promise((resolve, reject) => {
    setTimeout(() => {
        let success = true; // Giả lập điều kiện thành công hoặc thất bại
        if (success) {
            resolve("Dữ liệu nhận được thành công!");
        } else {
            reject("Lỗi khi lấy dữ liệu!");
        }
    }, 2000);
});

// Sử dụng Promise với .then() và .catch()
fetchData
    .then((result) => {
        console.log("Thành công:", result);
    })
    .catch((error) => {
        console.log("Thất bại:", error);
    });

Giải thích ví dụ

  • Tạo một Promise: Promise mô phỏng một tác vụ bất đồng bộ (đợi 2 giây).

  • Sử dụng setTimeout: Giả lập độ trễ như khi gửi yêu cầu API.

  • Gọi resolve(): Nếu tác vụ thành công, Promise chuyển sang fulfilled và trả về "Dữ liệu nhận được thành công!".

  • Gọi reject(): Nếu thất bại, Promise chuyển sang rejected và trả về "Lỗi khi lấy dữ liệu!".

  • Xử lý với .then(): Chạy khi Promise fulfilled.

  • Xử lý với .catch(): Chạy khi Promise rejected.

Kết quả mong đợi (sau 2 giây):

  • Nếu success = true: "Thành công: Dữ liệu nhận được thành công!"

  • Nếu success = false: "Thất bại: Lỗi khi lấy dữ liệu!"

Cách tạo một Promise trong JavaScript

Cú pháp khai báo một Promise

Trong JavaScript, một Promise được tạo bằng cách sử dụng new Promise(), trong đó ta truyền vào một executor function chứa hai tham số:

const myPromise = new Promise((resolve, reject) => {
    // Thực hiện một tác vụ bất đồng bộ (ví dụ: gọi API, đọc file, ...)
    if (/* Thành công */) {
        resolve(giá_trị_trả_về);
    } else {
        reject(lý_do_lỗi);
    }
});
  • resolve(value): Được gọi khi tác vụ hoàn thành thành công và trả về một giá trị.

  • reject(error): Được gọi khi có lỗi xảy ra hoặc tác vụ thất bại.

Giải thích tham số resolvereject

resolve(value)

  • Khi một Promise hoàn tất thành công, nó sẽ gọi resolve(value) để chuyển trạng thái từ pending sang fulfilled.

  • value có thể là bất kỳ kiểu dữ liệu nào (chuỗi, số, object, array, v.v.).

reject(error)

  • Khi có lỗi hoặc thất bại, reject(error) được gọi để chuyển trạng thái từ pending sang rejected.

  • error thường là một chuỗi mô tả lỗi hoặc một đối tượng Error.

Ví dụ về một Promise đơn giản

Ví dụ 1: Mô phỏng yêu cầu API với setTimeout()

Dưới đây là một Promise mô phỏng việc lấy dữ liệu từ máy chủ bằng cách sử dụng setTimeout() để giả lập độ trễ:

const fetchData = new Promise((resolve, reject) => {
    console.log("Đang lấy dữ liệu...");

    setTimeout(() => {
        let success = true; // Giả lập thành công hay thất bại

        if (success) {
            resolve("Dữ liệu nhận được thành công!");
        } else {
            reject("Lỗi khi lấy dữ liệu!");
        }
    }, 2000);
});

// Sử dụng Promise
fetchData
    .then((result) => {
        console.log("Thành công:", result);
    })
    .catch((error) => {
        console.log("Thất bại:", error);
    });

Giải thích:

  • Khi chạy, JavaScript sẽ in "Đang lấy dữ liệu...".

  • Sau 2 giây, nếu success = true, Promise sẽ gọi resolve("Dữ liệu nhận được thành công!").

  • Nếu success = false, Promise sẽ gọi reject("Lỗi khi lấy dữ liệu!").

  • .then() xử lý kết quả thành công.

  • .catch() xử lý lỗi nếu có.

Kết quả mong đợi sau 2 giây:

  • Nếu success = true: "Thành công: Dữ liệu nhận được thành công!"

  • Nếu success = false: "Thất bại: Lỗi khi lấy dữ liệu!"

Ví dụ 2: Kiểm tra số chẵn/lẻ bằng Promise

function checkEvenNumber(number) {
    return new Promise((resolve, reject) => {
        if (number % 2 === 0) {
            resolve(`${number} là số chẵn.`);
        } else {
            reject(`${number} là số lẻ.`);
        }
    });
}

// Gọi hàm và xử lý kết quả bằng .then() và .catch()
checkEvenNumber(10)
    .then(result => console.log("Thành công:", result))
    .catch(error => console.log("Thất bại:", error));

checkEvenNumber(7)
    .then(result => console.log("Thành công:", result))
    .catch(error => console.log("Thất bại:", error));

Giải thích:

  • Nếu số là chẵn, Promise gọi resolve() và kết quả hiển thị trong .then().

  • Nếu số là lẻ, Promise gọi reject() và kết quả hiển thị trong .catch().

Kết quả mong đợi:

Thành công: 10 là số chẵn.
Thất bại: 7 là số lẻ.

Xử lý Promise bằng .then(), .catch(), và .finally() trong JavaScript

Trong JavaScript, sau khi một Promise được tạo ra, chúng ta có thể sử dụng .then(), .catch(), và .finally() để xử lý kết quả của nó.

.then(): Xử lý khi Promise thành công

Cách hoạt động của .then()

  • .then(callback) được gọi khi Promise thành công (fulfilled).

  • Callback function trong .then() nhận giá trị trả về từ resolve(value).

Ví dụ 1: Dùng .then() để lấy kết quả từ Promise

const getUser = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("Người dùng: Nguyễn Văn A");
    }, 2000);
});

// Xử lý khi Promise thành công
getUser.then((data) => {
    console.log("Thành công:", data);
});

Giải thích:

  • Sau 2 giây, Promise gọi resolve("Người dùng: Nguyễn Văn A").

  • .then() nhận giá trị "Người dùng: Nguyễn Văn A" và hiển thị trên console.

Kết quả sau 2 giây:

Thành công: Người dùng: Nguyễn Văn A

.catch(): Xử lý khi Promise bị từ chối (rejected)

Cách hoạt động của .catch()

  • .catch(callback) được gọi khi Promise bị từ chối (rejected).

  • Callback function trong .catch() nhận lỗi từ reject(error).

Ví dụ 2: Dùng .catch() để xử lý lỗi

const fetchData = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject("Lỗi: Không thể kết nối đến server!");
    }, 2000);
});

// Xử lý khi Promise bị từ chối
fetchData.catch((error) => {
    console.log("Thất bại:", error);
});
Kết quả sau 2 giây:
Thất bại: Lỗi: Không thể kết nối đến server!

.finally(): Luôn chạy sau khi Promise kết thúc

Cách hoạt động của .finally()

  • .finally(callback) chạy bất kể Promise thành công hay thất bại.

  • Không nhận giá trị từ resolve() hoặc reject(), chỉ dùng để thực hiện tác vụ cuối cùng.

Ví dụ 3: Dùng .finally() để dọn dẹp tài nguyên

const processOrder = new Promise((resolve, reject) => {
    let success = Math.random() > 0.5; // Xác suất thành công 50%
    setTimeout(() => {
        if (success) {
            resolve("Đơn hàng đã xử lý!");
        } else {
            reject("Lỗi: Đơn hàng bị hủy!");
        }
    }, 2000);
});

processOrder
    .then((message) => console.log("Thành công:", message))
    .catch((error) => console.log("Thất bại:", error))
    .finally(() => console.log("Kết thúc quá trình xử lý đơn hàng."));
Kết quả có thể là một trong hai trường hợp:
Thành công: Đơn hàng đã xử lý!
Kết thúc quá trình xử lý đơn hàng.
hoặc
Thất bại: Lỗi: Đơn hàng bị hủy!
Kết thúc quá trình xử lý đơn hàng.

Lưu ý: .finally() luôn chạy, dù Promise thành công hay thất bại.

Chuỗi Promise (Promise Chaining)

Giúp xử lý nhiều tác vụ bất đồng bộ theo thứ tự.

Tránh Callback Hell khi sử dụng callback lồng nhau.

Ví dụ 4: Chuỗi Promise xử lý tuần tự

function step1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log("Bước 1: Chuẩn bị nguyên liệu");
            resolve("Nguyên liệu đã sẵn sàng");
        }, 1000);
    });
}

function step2(previousStep) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log(`Bước 2: Chế biến (${previousStep})`);
            resolve("Món ăn đã hoàn thành");
        }, 1000);
    });
}

function step3(finalStep) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log(`Bước 3: Dọn dẹp (${finalStep})`);
            resolve("Quá trình nấu ăn kết thúc");
        }, 1000);
    });
}

// Xâu chuỗi các bước thực hiện
step1()
    .then(step2)
    .then(step3)
    .then((result) => console.log("", result))
    .catch((error) => console.log("", error));
Kết quả hiển thị theo thứ tự sau (mỗi bước cách 1 giây):
Bước 1: Chuẩn bị nguyên liệu
Bước 2: Chế biến (Nguyên liệu đã sẵn sàng)
Bước 3: Dọn dẹp (Món ăn đã hoàn thành)
 Quá trình nấu ăn kết thúc

Kết hợp nhiều Promise với Promise.all(), Promise.race(), Promise.allSettled(), Promise.any()

Khi làm việc với nhiều tác vụ bất đồng bộ, JavaScript cung cấp các phương thức giúp xử lý nhiều Promise cùng lúc. Các phương thức này giúp tối ưu thời gian thực thi, cải thiện hiệu suất, và quản lý lỗi tốt hơn.

Promise.all() – Chạy nhiều Promise song song

Cách hoạt động của Promise.all()

  • Promise.all([promise1, promise2, ...]) chạy tất cả Promise cùng lúc.

  • Chỉ trả về kết quả khi TẤT CẢ các Promise đều thành công.

  • Nếu một Promise bị lỗi (reject), cả Promise.all() sẽ bị từ chối (reject).

Ví dụ 1: Sử dụng Promise.all() để chạy nhiều Promise đồng thời

const p1 = new Promise((resolve) => setTimeout(() => resolve(" Táo"), 2000));
const p2 = new Promise((resolve) => setTimeout(() => resolve(" Chuối"), 1000));
const p3 = new Promise((resolve) => setTimeout(() => resolve(" Nho"), 1500));

Promise.all([p1, p2, p3])
    .then((results) => console.log(" Thành công:", results))
    .catch((error) => console.log(" Lỗi:", error));

Kết quả sau 2 giây (khi tất cả Promise hoàn tất):

 Thành công: [" Táo", " Chuối", " Nho"]

Ví dụ 2: Promise.all() bị từ chối nếu một Promise thất bại

const p1 = new Promise((resolve) => setTimeout(() => resolve(" Thành công 1"), 2000));
const p2 = new Promise((_, reject) => setTimeout(() => reject(" Lỗi 2"), 1000));
const p3 = new Promise((resolve) => setTimeout(() => resolve(" Thành công 3"), 1500));

Promise.all([p1, p2, p3])
    .then((results) => console.log(" Thành công:", results))
    .catch((error) => console.log(" Lỗi:", error));
Kết quả sau 1 giây (khi p2 bị từ chối):
 Lỗi: Lỗi 2

Lưu ý: Khi một Promise bị từ chối, Promise.all() ngay lập tức bị reject, không đợi các Promise khác hoàn tất.

Promise.race() – Trả về Promise hoàn thành đầu tiên

Cách hoạt động của Promise.race()

  • Promise.race([promise1, promise2, ...]) trả về kết quả của Promise hoàn thành đầu tiên.

  • Dù Promise thành công hay thất bại, kết quả sẽ là Promise chạy nhanh nhất.

Ví dụ 3: Promise.race() lấy kết quả nhanh nhất

const p1 = new Promise((resolve) => setTimeout(() => resolve(" P1 thắng!"), 3000));
const p2 = new Promise((resolve) => setTimeout(() => resolve(" P2 thắng!"), 2000));
const p3 = new Promise((resolve) => setTimeout(() => resolve(" P3 thắng!"), 1000));

Promise.race([p1, p2, p3])
    .then((result) => console.log(" Kết quả:", result))
    .catch((error) => console.log(" Lỗi:", error));

Kết quả sau 1 giây (P3 hoàn thành trước):

 Kết quả:  P3 thắng!

Ví dụ 4: Promise.race() với Promise bị từ chối

const p1 = new Promise((_, reject) => setTimeout(() => reject(" P1 thất bại!"), 1500));
const p2 = new Promise((resolve) => setTimeout(() => resolve(" P2 thành công!"), 2000));

Promise.race([p1, p2])
    .then((result) => console.log("Kết quả:", result))
    .catch((error) => console.log(" Lỗi:", error));

Kết quả sau 1.5 giây (p1 bị từ chối trước):

Lỗi: P1 thất bại!

Lưu ý: Nếu Promise đầu tiên bị từ chối, Promise.race() cũng bị từ chối ngay lập tức.

Promise.allSettled() – Trả về tất cả kết quả, dù thành công hay thất bại

Cách hoạt động của Promise.allSettled()

  • Promise.allSettled([promise1, promise2, ...]) luôn trả về tất cả kết quả của các Promise.

  • Không bị ảnh hưởng nếu một số Promise thất bại.

  • Kết quả là một mảng các object, chứa {status: "fulfilled" | "rejected", value | reason}.

Ví dụ 5: Promise.allSettled() luôn trả về tất cả kết quả

const p1 = new Promise((resolve) => setTimeout(() => resolve(" P1 hoàn thành"), 1000));
const p2 = new Promise((_, reject) => setTimeout(() => reject(" P2 thất bại"), 2000));
const p3 = new Promise((resolve) => setTimeout(() => resolve("P3 hoàn thành"), 1500));

Promise.allSettled([p1, p2, p3])
    .then((results) => console.log("Kết quả:", results));

Kết quả sau 2 giây:

 Kết quả: [
  { status: "fulfilled", value: " P1 hoàn thành" },
  { status: "rejected", reason: " P2 thất bại" },
  { status: "fulfilled", value: "P3 hoàn thành" }
]

Lợi ích: Promise.allSettled() giúp kiểm tra trạng thái của từng Promise mà không bị dừng lại nếu có lỗi.

Promise.any() – Trả về Promise đầu tiên được resolve()

Cách hoạt động của Promise.any()

  • Promise.any([promise1, promise2, ...]) trả về kết quả của Promise đầu tiên được resolve().

  • Nếu tất cả các Promise bị từ chối, nó trả về AggregateError.

Ví dụ 6: Promise.any() lấy kết quả đầu tiên thành công

const p1 = new Promise((_, reject) => setTimeout(() => reject(" P1 thất bại"), 1000));
const p2 = new Promise((resolve) => setTimeout(() => resolve(" P2 thành công"), 2000));
const p3 = new Promise((resolve) => setTimeout(() => resolve(" P3 thành công"), 3000));

Promise.any([p1, p2, p3])
    .then((result) => console.log(" Kết quả:", result))
    .catch((error) => console.log(" Lỗi:", error));

Kết quả sau 2 giây (P2 hoàn thành trước):

 Kết quả: 

Xử lý lỗi trong Promise trong JavaScript

Khi làm việc với Promise, lỗi có thể xảy ra do nhiều nguyên nhân, như lỗi mạng, API trả về lỗi, hoặc lỗi logic trong code. JavaScript cung cấp các cách để bắt lỗi và xử lý chúng một cách hiệu quả.

Bắt lỗi bằng .catch()

  • .catch() được sử dụng để xử lý lỗi khi một Promise bị reject.

  • Nếu bất kỳ bước nào trong chuỗi .then() gặp lỗi, .catch() sẽ bắt lỗi đó.

Ví dụ 1: .catch() xử lý lỗi khi API bị lỗi

fetch("https://api.example.com/data") // Gọi API
    .then((response) => response.json()) // Chuyển dữ liệu sang JSON
    .then((data) => console.log(" Dữ liệu:", data)) 
    .catch((error) => console.log(" Lỗi xảy ra:", error)); // Xử lý lỗi

Nếu API bị lỗi (ví dụ: sai URL hoặc mất kết nối), .catch() sẽ chạy.

Ví dụ 2: .catch() bắt lỗi trong chuỗi Promise

const getData = () => {
    return new Promise((_, reject) => {
        setTimeout(() => reject(" Không thể lấy dữ liệu!"), 2000);
    });
};

getData(
    .then((data) => console.log(" Dữ liệu:", data))
    .catch((error) => console.log(" Lỗi:", error));

Sau 2 giây, Promise bị reject và .catch() chạy:

Lỗi: Không thể lấy dữ liệu!

Xử lý lỗi với try...catch trong async/await

  • Khi sử dụng async/await, bạn có thể dùng try...catch để bắt lỗi một cách rõ ràng và dễ đọc hơn.

  • Ưu điểm: Không cần dùng .then().catch(), giúp code dễ đọc hơn.

Ví dụ 3: Sử dụng try...catch để bắt lỗi khi gọi API

const fetchData = async () => {
    try {
        let response = await fetch("https://api.example.com/data");
        let data = await response.json();
        console.log(" Dữ liệu:", data);
    } catch (error) {
        console.log(" Lỗi xảy ra:", error);
    }
};

fetchData();

Nếu API bị lỗi, catch sẽ chạy thay vì làm hỏng toàn bộ chương trình.

Ví dụ 4: try...catch bắt lỗi trong Promise bị reject

const getData = () => {
    return new Promise((_, reject) => {
        setTimeout(() => reject(" Lỗi khi tải dữ liệu!"), 2000);
    });
};

const fetchData = async () => {
    try {
        let data = await getData();
        console.log(" Dữ liệu:", data);
    } catch (error) {
        console.log(" Lỗi:", error);
    }
};

fetchData();

Sau 2 giây, lỗi sẽ bị bắt và in ra màn hình.

Promise và Async/Await trong JavaScript

So sánh Promise và Async/Await

Tiêu chí Promise (.then() / .catch()) Async/Await
Cách viết Dùng .then() để xử lý kết quả, .catch() để bắt lỗi. Dùng await để đợi kết quả, try...catch để bắt lỗi.
Độ dễ đọc Khó đọc hơn khi có nhiều .then() lồng nhau. Dễ đọc hơn, giống code đồng bộ.
Xử lý lỗi .catch() xử lý lỗi. try...catch xử lý lỗi dễ dàng hơn.
Quản lý nhiều Promise Dùng Promise.all(), Promise.race(),... Dùng await Promise.all().

Chuyển từ .then() sang async/await

Ví dụ 5: Xử lý bất đồng bộ bằng .then()

fetch("https://api.example.com/data")
    .then((response) => response.json())
    .then((data) => console.log("Dữ liệu:", data))
    .catch((error) => console.log(" Lỗi:", error));

Nhược điểm: Khi có nhiều .then(), code có thể trở nên khó đọc.

Ví dụ 6: Viết lại bằng async/await

const fetchData = async () => {
    try {
        let response = await fetch("https://api.example.com/data");
        let data = await response.json();
        console.log(" Dữ liệu:", data);
    } catch (error) {
        console.log(" Lỗi:", error);
    }
};

fetchData();

Kết hợp Promise.all() với async/await

Ví dụ 7: Chạy nhiều API song song bằng Promise.all()

const fetchData1 = () => fetch("https://api.example.com/data1").then((res) => res.json());
const fetchData2 = () => fetch("https://api.example.com/data2").then((res) => res.json());

const getData = async () => {
    try {
        let [data1, data2] = await Promise.all([fetchData1(), fetchData2()]);
        console.log(" Data 1:", data1);
        console.log(" Data 2:", data2);
    } catch (error) {
        console.log(" Lỗi:", error);
    }
};

getData();

Kết bài

Promises là một phần quan trọng trong JavaScript, giúp xử lý bất đồng bộ một cách rõ ràng và hiệu quả. Chúng cho phép quản lý các tác vụ như gọi API, đọc/ghi dữ liệu, hoặc xử lý sự kiện một cách có tổ chức hơn so với callback truyền thống.

  • Promise giúp xử lý bất đồng bộ và có ba trạng thái chính: pending, fulfilled, rejected.

  • Sử dụng .then(), .catch(), .finally() để xử lý thành công, lỗi và tác vụ kết thúc.

  • Kết hợp nhiều Promise với Promise.all(), Promise.race(), Promise.allSettled(), Promise.any() để tối ưu hiệu suất.

  • async/await giúp viết code dễ đọc hơn, và kết hợp với try...catch để xử lý lỗi tốt hơn.

Bằng cách hiểu rõ cách sử dụng Promise và async/await, bạn có thể viết mã JavaScript mạnh mẽ, dễ bảo trì và tối ưu hơn, giúp ứng dụng hoạt động hiệu quả ngay cả khi xử lý nhiều tác vụ đồng thời.

Bài viết liên quan