NTM Solutions

Bài đăng nổi bật

🚀 Laravel 12 (2026) — Từ PHP Core đến Full Stack📘

Nếu cập nhật theo Laravel 12 LTS (2026) thì mình sẽ chia thành các phần như sau để người học đi từ PHP Core → Laravel → Triển khai dự án th...

Chủ Nhật, 18 tháng 10, 2026

🚀Bài 05 — Promise & Async/Await trong JavaScript

1. Vì sao cần học Promise?

Trong JavaScript, có những công việc hoàn thành gần như ngay lập tức:

const name = 'Doraemon';

console.log(name);

Nhưng cũng có những công việc cần thời gian chờ:

  • Gọi Laravel REST API.

  • Tải danh sách bài viết từ Database thông qua API.

  • Đọc dữ liệu từ máy chủ.

  • Tải hình ảnh.

  • Gửi Form đăng nhập.

  • Chờ kết quả từ một dịch vụ bên ngoài.

Ví dụ, khi ứng dụng cần lấy danh sách bài viết từ Laravel:

JavaScript
    │
    │ Gửi yêu cầu
    ▼
Laravel REST API
    │
    ▼
Controller
    │
    ▼
Database
    │
    │ Trả kết quả
    ▼
JavaScript nhận dữ liệu

Quá trình này có thể mất một khoảng thời gian. 

JavaScript cần cơ chế xử lý công việc bất đồng bộ mà không phải chặn toàn bộ hoạt động của trình duyệt trong lúc chờ.

Hai kiến thức quan trọng chúng ta cần học là:

  • Promise

  • async/await


2. JavaScript đồng bộ và bất đồng bộ

2.1. Đồng bộ — Synchronous

Ví dụ:

console.log('Doraemon');

console.log('Nobita');

console.log('Pikachu');

Kết quả:

Doraemon
Nobita
Pikachu

Các câu lệnh được thực thi theo thứ tự.

2.2. Bất đồng bộ — Asynchronous

Ví dụ:

console.log('Doraemon');

setTimeout(() => {
    console.log('Nobita');
}, 2000);

console.log('Pikachu');

Kết quả:

Doraemon
Pikachu
Nobita

setTimeout() lên lịch thực thi callback sau khoảng thời gian chỉ định. 

Trong thời gian đó, JavaScript tiếp tục thực hiện các công việc khác.

Điều quan trọng cần hiểu là: bất đồng bộ không có nghĩa là JavaScript luôn chạy nhiều đoạn code cùng lúc. 

JavaScript trong trình duyệt thường thực thi code trên một luồng chính, trong khi các thao tác như I/O có thể tiến hành mà không chặn luồng đó.


3. Callback là gì?

Callback là một hàm được truyền vào một hàm khác để được gọi sau đó.

Ví dụ:

function greet(name, callback) {
    console.log(`Xin chào ${name}`);

    callback();
}

greet('Doraemon', () => {
    console.log('Bắt đầu học JavaScript');
});

Kết quả:

Xin chào Doraemon
Bắt đầu học JavaScript

Callback cũng xuất hiện trong các thao tác bất đồng bộ:

console.log('Bắt đầu');

setTimeout(() => {
    console.log('Đã hoàn thành');
}, 1000);

console.log('Tiếp tục công việc');

Callback có thể xử lý những trường hợp đơn giản. 

Tuy nhiên, khi nhiều công việc phụ thuộc lẫn nhau, code có thể trở nên khó đọc nếu lồng nhiều callback.

Đây là một trong những lý do Promise ra đời.


4. Promise là gì?

Promise là một đối tượng đại diện cho kết quả của một công việc bất đồng bộ, có thể hoàn thành thành công hoặc thất bại.

Một Promise có ba trạng thái:

Pending
Đang chờ kết quả
✅
Fulfilled
Hoàn thành thành công
❌
Rejected
Thất bại

Ví dụ tưởng tượng:

Doraemon gửi yêu cầu lấy bánh
             │
             ▼
          Pending
             │
       ┌─────┴─────┐
       ▼           ▼
  Có bánh      Hết bánh
       │           │
       ▼           ▼
  Fulfilled    Rejected

Đây chỉ là ví dụ minh họa. 

Trong ứng dụng thật, Promise có thể đại diện cho một yêu cầu API, một thao tác đọc file hoặc một công việc bất đồng bộ khác.


5. Tạo Promise đầu tiên

Chúng ta có thể tự tạo Promise bằng new Promise().

const promise = new Promise((resolve, reject) => {
    const hasCake = true;

    if (hasCake) {
        resolve('Doraemon đã nhận được bánh!');
    } else {
        reject(new Error('Đã hết bánh!'));
    }
});

Hai hàm quan trọng:

  • resolve(value): hoàn thành Promise thành công.

  • reject(error): hoàn thành Promise với trạng thái thất bại.

Để nhận kết quả, chúng ta sử dụng .then() và .catch().

promise
    .then(result => {
        console.log(result);
    })
    .catch(error => {
        console.error(error.message);
    });

Kết quả:

Doraemon đã nhận được bánh!

Lưu ý: 

  • Promise chỉ chuyển từ pending sang trạng thái cuối cùng một lần. 
  • Sau khi đã fulfilled hoặc rejected, nó không chuyển sang trạng thái khác.


6. .then() — nhận kết quả thành công

Ví dụ:

const getScore = new Promise(resolve => {
    resolve(100);
});

getScore.then(score => {
    console.log(`Điểm của Pikachu: ${score}`);
});

Kết quả:

Điểm của Pikachu: 100

.then() nhận kết quả khi Promise hoàn thành thành công.

Có thể nối nhiều bước xử lý:

Promise.resolve(10)
    .then(number => number * 2)
    .then(number => number + 5)
    .then(result => {
        console.log(result);
    });

Kết quả:

25

Mỗi .then() có thể trả về một giá trị hoặc một Promise. 

Giá trị đó sẽ được chuyển sang bước tiếp theo.

Đây là cơ chế Promise chaining — nối chuỗi Promise.


7. .catch() — xử lý lỗi

Ví dụ:

const promise = new Promise((resolve, reject) => {
    reject(new Error('Conan không tìm thấy dữ liệu!'));
});

promise
    .then(result => {
        console.log(result);
    })
    .catch(error => {
        console.error(error.message);
    });

Kết quả:

Conan không tìm thấy dữ liệu!

.catch() xử lý trường hợp Promise bị reject hoặc lỗi phát sinh trong các bước xử lý trước đó của chuỗi Promise.

Trong ứng dụng Laravel, lỗi có thể xuất phát từ:

  • API không truy cập được.

  • Server trả về lỗi.

  • Dữ liệu không đúng định dạng mong đợi.

  • Code xử lý dữ liệu phát sinh exception.

Tuy nhiên, một điểm quan trọng: fetch() không tự reject Promise chỉ vì máy chủ trả về HTTP 404 hoặc 500. 

Chúng ta sẽ xử lý trường hợp đó ở Bài 06.


8. .finally() — luôn thực hiện bước kết thúc

Giả sử trong lúc tải dữ liệu, giao diện hiển thị:

Đang tải dữ liệu...

Khi công việc kết thúc, dù thành công hay thất bại, chúng ta muốn ẩn thông báo tải.

Có thể dùng .finally():

Promise.resolve('Dữ liệu của Doraemon')
    .then(result => {
        console.log(result);
    })
    .catch(error => {
        console.error(error.message);
    })
    .finally(() => {
        console.log('Kết thúc quá trình xử lý');
    });

.finally() thường được dùng để:

  • Ẩn trạng thái Loading.

  • Bật lại nút Submit.

  • Dọn dẹp tài nguyên.

  • Thực hiện công việc kết thúc.

Giá trị trả về từ .finally() thông thường không thay thế kết quả của Promise trước đó, trừ khi callback trong .finally() phát sinh lỗi hoặc trả về Promise bị reject.


9. Ví dụ Promise với setTimeout()

Tạo 01 hàm giả lập việc tải dữ liệu từ máy chủ:

function getCharacter() {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve({
                name: 'Doraemon',
                power: 100
            });
        }, 2000);
    });
}

getCharacter().then(character => {
    console.log(character);
});

Sau khoảng hai giây, Console hiển thị:

{
    name: 'Doraemon',
    power: 100
}

Hàm getCharacter() trả về một Promise ngay lập tức. 

Giá trị dữ liệu chỉ được cung cấp khi Promise hoàn thành.

Đây là mô hình gần giống việc chờ một API trả dữ liệu.


10. Async/Await là gì?

async/await là cú pháp giúp viết code sử dụng Promise theo cách dễ đọc hơn.

Thay vì viết:

getCharacter()
    .then(character => {
        console.log(character.name);
    })
    .catch(error => {
        console.error(error.message);
    });

Chúng ta có thể viết:

async function showCharacter() {
    try {
        const character = await getCharacter();

        console.log(character.name);
    } catch (error) {
        console.error(error.message);
    }
}

showCharacter();

Kết quả vẫn là:

Doraemon

Sự khác biệt chủ yếu nằm ở cách tổ chức code.

async/await không thay thế Promise. 

Nó sử dụng Promise và giúp chúng ta viết các bước bất đồng bộ dễ theo dõi hơn.


11. Từ khóa async

Khi khai báo một hàm bằng async, hàm đó luôn trả về một Promise.

Ví dụ:

async function sayHello() {
    return 'Xin chào Doraemon!';
}

sayHello().then(message => {
    console.log(message);
});

Kết quả:

Xin chào Doraemon!

Ngay cả khi chúng ta trả về một chuỗi thông thường, JavaScript cũng bọc kết quả đó trong Promise đã hoàn thành thành công.

Có thể viết Arrow Function với async:

const sayHello = async () => {
    return 'Xin chào Pikachu!';
};

sayHello().then(console.log);

Cú pháp này sẽ xuất hiện thường xuyên khi viết Vue và React.


12. Từ khóa await

await dùng để chờ một Promise hoàn thành và lấy kết quả của nó bên trong hàm async.

Ví dụ:

function getPower() {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(100);
        }, 1000);
    });
}

async function showPower() {
    console.log('Đang kiểm tra sức mạnh...');

    const power = await getPower();

    console.log(`Sức mạnh của Pikachu: ${power}`);
}

showPower();

console.log('JavaScript vẫn tiếp tục thực hiện công việc khác');

Thứ tự kết quả:

Đang kiểm tra sức mạnh...
JavaScript vẫn tiếp tục thực hiện công việc khác
Sức mạnh của Pikachu: 100

Khi chạy đến await, hàm showPower() tạm dừng tại đó để chờ Promise, nhưng luồng JavaScript vẫn có thể thực hiện những công việc khác.

Lưu ý: 

  • await thường được sử dụng bên trong hàm async. 
  • JavaScript cũng hỗ trợ top-level await trong ES Modules ở những môi trường tương thích, nhưng trong bài này chúng ta sử dụng await bên trong hàm để dễ tổ chức code.


13. Xử lý lỗi với try/catch

Khi sử dụng await, chúng ta có thể dùng try/catch để xử lý lỗi.

function getCake() {
    return new Promise((resolve, reject) => {
        reject(new Error('Không còn bánh cho Nobita!'));
    });
}

async function receiveCake() {
    try {
        const cake = await getCake();

        console.log(cake);
    } catch (error) {
        console.error(error.message);
    }
}

receiveCake();

Kết quả:

Không còn bánh cho Nobita!

Cách này giúp chúng ta tổ chức code rõ ràng hơn khi có nhiều thao tác bất đồng bộ.

Ví dụ với Laravel API:

async function loadPosts() {
    try {
        // Gọi API tại đây.
    } catch (error) {
        // Xử lý lỗi tại đây.
    }
}

Ở Bài 06, chúng ta sẽ thay phần giả lập bằng fetch() để gọi API thật.


14. So sánh Promise và Async/Await

Tiêu chíPromise chainingAsync/Await
Nhận kết quả.then()await
Xử lý lỗi.catch()try/catch
Dọn dẹp cuối quá trình.finally()Có thể dùng finally trong try/catch/finally
Phù hợpChuỗi xử lý ngắnQuy trình nhiều bước
Bản chấtPromiseVẫn sử dụng Promise

02 cách đều hợp lệ.

Ví dụ Promise chaining:

getCharacter()
    .then(character => character.name)
    .then(name => console.log(name))
    .catch(console.error);

Ví dụ Async/Await:

async function showName() {
    try {
        const character = await getCharacter();

        console.log(character.name);
    } catch (error) {
        console.error(error);
    }
}

showName();

Trong các bài tiếp theo, chúng ta sẽ ưu tiên async/await cho các ví dụ gọi API vì cách viết này dễ theo dõi khi có nhiều bước xử lý.


15. Chạy nhiều công việc với Promise.all()

Giả sử cần lấy thông tin của ba nhân vật.

function getCharacter(name, delay) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(name);
        }, delay);
    });
}

async function loadCharacters() {
    const characters = await Promise.all([
        getCharacter('Doraemon', 2000),
        getCharacter('Pikachu', 1000),
        getCharacter('Conan', 1500)
    ]);

    console.log(characters);
}

loadCharacters();

Kết quả:

['Doraemon', 'Pikachu', 'Conan']

Thứ tự kết quả vẫn theo thứ tự các Promise được truyền vào Promise.all(), không phụ thuộc vào thứ tự hoàn thành.

Trong ví dụ này, ba công việc được bắt đầu gần như cùng lúc, nên tổng thời gian chờ thường xấp xỉ công việc lâu nhất thay vì tổng thời gian của cả ba.

Khi nào dùng Promise.all()?

Khi cần thực hiện nhiều công việc độc lập, chẳng hạn:

  • Tải danh sách bài viết.

  • Tải danh mục.

  • Tải thông tin thống kê.

Nếu một Promise bị reject, Promise tổng hợp từ Promise.all() cũng reject. 

Nếu cần nhận kết quả thành công và thất bại riêng của từng công việc, chúng ta có thể tìm hiểu Promise.allSettled() ở phần mở rộng sau này.


16. Promise tuần tự và Promise song song

Hai cách viết dưới đây có ý nghĩa khác nhau.

Cách 1 — Chờ tuần tự

async function loadData() {
    const a = await getCharacter('Doraemon', 1000);
    const b = await getCharacter('Pikachu', 1000);

    console.log(a, b);
}

Công việc thứ hai chỉ bắt đầu sau khi công việc thứ nhất hoàn thành.

Cách 2 — Chờ song song

async function loadData() {
    const promiseA = getCharacter('Doraemon', 1000);
    const promiseB = getCharacter('Pikachu', 1000);

    const [a, b] = await Promise.all([
        promiseA,
        promiseB
    ]);

    console.log(a, b);
}

Hai Promise được tạo trước khi await chờ kết quả. 

Vì vậy, các công việc có thể tiến hành đồng thời.

Quy tắc: 

  • nếu công việc B cần kết quả của A, hãy xử lý tuần tự. 
  • Nếu A và B độc lập, có thể cân nhắc chạy song song.


17. Thực hành trong Laravel 13

Chúng ta sẽ tạo một trang đơn giản mô phỏng việc tải danh sách bài viết.

Ở bài này, dữ liệu vẫn là dữ liệu giả lập. Chúng ta chưa cần Laravel API thật.

Bước 1 — Tạo file resources/js/modules/posts.js

export function getPosts() {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve([
                {
                    id: 1,
                    title: 'Laravel 13',
                    author: 'Doraemon'
                },
                {
                    id: 2,
                    title: 'JavaScript hiện đại',
                    author: 'Nobita'
                },
                {
                    id: 3,
                    title: 'Vue cơ bản',
                    author: 'Pikachu'
                }
            ]);
        }, 1500);
    });
}

Hàm getPosts() trả về một Promise, mô phỏng API mất khoảng 1,5 giây để trả dữ liệu.

Bước 2 — Tạo file resources/js/app.js

import { getPosts } from './modules/posts.js';

const postList = document.querySelector('#postList');
const loading = document.querySelector('#loading');
const errorMessage = document.querySelector('#errorMessage');

async function loadPosts() {
    try {
        if (loading) {
            loading.hidden = false;
        }

        if (errorMessage) {
            errorMessage.textContent = '';
        }

        const posts = await getPosts();

        if (!postList) {
            return;
        }

        postList.replaceChildren();

        posts.forEach(post => {
            const li = document.createElement('li');
            const title = document.createElement('strong');
            const author = document.createElement('span');

            title.textContent = post.title;
            author.textContent = ` — ${post.author}`;

            li.append(title, author);
            postList.append(li);
        });
    } catch (error) {
        if (errorMessage) {
            errorMessage.textContent = error.message;
        }
    } finally {
        if (loading) {
            loading.hidden = true;
        }
    }
}

loadPosts();

Trong ví dụ này:

  • async khai báo hàm bất đồng bộ.

  • await chờ dữ liệu.

  • try/catch xử lý lỗi.

  • finally ẩn trạng thái Loading sau khi quá trình kết thúc.

Bước 3 — Tạo Blade View

Tạo file resources/views/javascript.blade.php:

<!DOCTYPE html>
<html lang="vi">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Promise & Async/Await</title>

    @vite(['resources/js/app.js'])
</head>
<body>
    <h1>Thư viện bài học</h1>

    <p id="loading" hidden>
        Đang tải dữ liệu...
    </p>

    <p id="errorMessage" role="alert"></p>

    <ul id="postList"></ul>
</body>
</html>

Nếu đã có trang /javascript từ những bài trước, hãy dùng lại trang đó hoặc tạo một view và Route mới để tránh khai báo Route trùng.

Bước 4 — Khởi chạy

Trong terminal thứ nhất:

npm run dev

Trong terminal thứ hai:

php artisan serve

Mở trang Laravel tương ứng trong trình duyệt.

Bạn sẽ thấy thông báo “Đang tải dữ liệu...” xuất hiện trong khoảng 1,5 giây. 

Sau đó, danh sách ba bài viết sẽ hiện ra.

Đây là cách mô phỏng trạng thái Loading mà chúng ta thường gặp khi chờ dữ liệu từ API.


18. Một số lỗi thường gặp

Lỗi 1 — Quên await

async function loadPosts() {
    const posts = getPosts();

    console.log(posts);
}

Biến posts lúc này chứa một Promise, chưa phải danh sách bài viết.

Cách đúng:

async function loadPosts() {
    const posts = await getPosts();

    console.log(posts);
}

Lỗi 2 — Dùng await nhưng không xử lý lỗi

async function loadPosts() {
    const posts = await getPosts();

    console.log(posts);
}

Nếu Promise bị reject, lỗi có thể trở thành một rejected Promise của hàm loadPosts() mà không được xử lý tại đây.

Nên chủ động dùng try/catch khi cần xử lý lỗi tại chỗ.

Lỗi 3 — Nghĩ rằng fetch() trả lỗi khi HTTP 404

fetch() thường vẫn hoàn thành thành công ở cấp độ Promise khi nhận được phản hồi HTTP 404 hoặc 500.

Muốn phát hiện lỗi HTTP, chúng ta cần kiểm tra thuộc tính response.ok. 

Nội dung này sẽ được thực hành trong Bài 06 — Fetch API.

Lỗi 4 — Dùng await bên trong forEach()

Ví dụ không nên dùng khi muốn chờ toàn bộ công việc hoàn thành:

posts.forEach(async post => {
    await savePost(post);
});

forEach() không chờ các Promise do callback async trả về.

Nếu cần chạy tuần tự, có thể dùng vòng lặp for...of:

for (const post of posts) {
    await savePost(post);
}

Nếu các công việc độc lập và có thể chạy song song:

await Promise.all(posts.map(post => savePost(post)));

Chỉ sử dụng cách thứ hai nếu hàm savePost() trả về Promise và việc chạy các thao tác song song là an toàn đối với ứng dụng.


19. Bài tập cuối bài

Bài tập 1 — Promise cơ bản

Tạo Promise trả về chuỗi:

Doraemon đã hoàn thành nhiệm vụ!

Sau đó dùng .then() để in kết quả.

Bài tập 2 — Xử lý lỗi

Tạo Promise bị reject với thông báo:

Pikachu không tìm thấy năng lượng!

Dùng .catch() để hiển thị lỗi.

Bài tập 3 — Async/Await

Viết hàm getCharacter() trả về Promise chứa thông tin:

{
    name: 'Conan',
    level: 10
}

Viết hàm showCharacter() sử dụng async/await để hiển thị tên và cấp độ.

Bài tập 4 — Loading

Dựa trên ví dụ Laravel ở phần 17, bổ sung một nút “Tải lại”. 

Khi bấm nút, danh sách được tải lại và trạng thái Loading được hiển thị trong lúc chờ.

Bài tập 5 — Promise.all

Tạo ba Promise trả về tên Doraemon, Nobita và Pikachu sau những khoảng thời gian khác nhau. 

Dùng Promise.all() để nhận kết quả và in ra Console.


20. Tổng kết

Kiến thứcCông dụng
CallbackTruyền hàm để thực hiện sau
PromiseĐại diện cho kết quả của công việc bất đồng bộ
.then()Xử lý kết quả thành công
.catch()Xử lý lỗi
.finally()Thực hiện bước kết thúc
asyncKhai báo hàm trả về Promise
awaitChờ Promise hoàn thành trong hàm bất đồng bộ
try/catchXử lý lỗi theo cấu trúc
Promise.all()Chờ nhiều Promise độc lập cùng hoàn thành

Điều cần nhớ nhất:

  1. Promise giúp xử lý các công việc bất đồng bộ.

  2. async/await là cách viết thuận tiện hơn khi làm việc với Promise.

  3. try/catch giúp xử lý lỗi khi chờ Promise bằng await.

  4. Promise.all() phù hợp khi cần chờ nhiều công việc độc lập.

  5. Khi gọi API thật, cần phân biệt lỗi mạng với lỗi HTTP.

Bài 06 — Fetch API: chúng ta sẽ dùng fetch() để gửi HTTP request, nhận JSON từ Laravel REST API, hiển thị Loading và xử lý lỗi thực tế.

Không có nhận xét nào:

Đăng nhận xét

Facebook Youtube RSS