Javascript là một ngôn ngữ hướng sự kiện và có khả năng hoạt động bất đồng bộ. Mình đã có một bài viết nói về cách thức hoạt động bất đồng bộ của JS, bạn có thể xem thêm Event Loop trong JS để hiểu thêm nhé.
Bài viết hôm nay, chúng ta hãy cùng tìm hiểu Callback, Promise và Async/Await trong Javascript. Đây là những kiến thức này nên và phải biết đối với một JS Developer 😎
Let’s get started !

Callback và Callback hell
Tuy không khó, nhưng khi mới học về JS ta sẽ thấy khái niệm này khá mông lung 😂. Để đơn giản, 1 thứ gì đó trong JS được gọi là callback khi nó thoả 2 điều kiện sau:
- Nó là một hàm (function).
- Nó là đối số cho một hàm khác.
Ví dụ:
// định nghĩa trước 1 hàm làm callback function callbackFn(params){ console.log(`Tui là con bét nè ${params}`); } // định nghĩa một hàm nào đó dùng callback function motHamNaoDo(str, cbFn){ // str là tham số bth, không phải callback // kiểm tra trước khi thực thi hàm if(typeof cbFn === "function"){ cbFn(str); } } // thực thi hàm vừa định nghĩa và truyền vào callback motHamNaoDo("hihi", callbackFn); // "Tui là con bét nè hihi"
Callback có lợi ích gì?
- Callback sẽ được gọi để thực thi bên trong một hàm khác. Điều này giúp chúng ta thực hiện một chuỗi các hành động (hàm) liên tiếp nhau.
- Callback rất hữu dụng khi xử lý sự kiện (event). Tức là chức năng đó sẽ được gọi ở một thời điểm nào đó khi event được kích hoạt. VD: click cái button thì hiển thị modal.
- Vì sao không định nghĩa chức năng đó ngay bên trong hàm luôn mà lại đi dùng callback?
- Vì callback được truyền vào hàm như một tham số. Điều này sẽ tăng tính linh động hơn, tuỳ vào trường hợp cụ thể mà bạn sẽ định nghĩa các hàm callback khác nhau để truyền vào.
- Callback còn thường được dùng trong các method của các object, array như các phương thức map, reduce, find, …
Một số lưu ý khi dùng callback
Bạn có thể định nghĩa trước một hàm và truyền nó vào như một callback. Hoặc truyền hẳn một anonymous function nếu không cần tái sử dụng.
const arr = [1, 5, 3, 2, 1]; // Cách 1: định nghĩa sẵn một hàm function ascendingSort(a, b){ return a - b; } arr.sort(ascendingSort); // Cách 2: truyền hẳn 1 anonymous function arr.sort((a, b) => a - b);
Khi định nghĩa một hàm có dùng callback, nhớ kiểm tra tham số truyền vào có phải là một hàm hay không bằng typeof params === 'function'
. Nếu không sẽ gây ra lỗi param is not a function.
Callback hell
Chắc mọi người đã nghe qua đâu đó thuật ngữ này. Đây là hiện tượng một chuỗi các callback được lồng vào nhau liên tiếp, dẫn đến việc khó theo dõi và debug code. Chúng ta có thể khắc phục điều này bằng Promise trong phần dưới.

Promise là gì?
Khái niệm Promise ra đời để giải quyết vấn đề Callback hell phía trên, nó giúp việc xử lý bất đồng độ trở nên dễ dàng, dễ code và dễ debug hơn khi chỉ sử dụng callback.
Lưu ý: điều này không có nghĩa là chúng ta không sử dụng callback nữa. Callback vẫn được sử dụng bên trong Promise, chỉ là chúng ta không cần dùng chúng lồng nhau khiến gây ra callback hell nữa.
Cơ chế hoạt động:
- khởi tạo một promise thông qua từ khoá new =>
new Promise()
. - Một promise nhận vào một hàm (executor) với 2 tham số cũng là 2 hàm là resolve và reject.
- Resolve dùng để trả dữ liệu về khi xử lý logic thành công.
- Reject dùng để trả lỗi khi xử lý thất bại.
- Dùng
.then(callback)
để nhận kết quả trả về từ resolve (thành công). - Dùng
.catch(callback)
để nhận kết quả trả về từ reject (bắt lỗi). - Dùng
.finally()
để xử lý logic trong mọi trường hợp sau khi này hoàn thành (luôn chạy).
Ví dụ:
// Định nghĩa một hàm fake lấy dữ liệu bất đồng bộ dùng Promise function fetchData(url){ return new Promise(function(resolve, reject){ setTimeout(()=>{ const data = Math.random(); if(data > 0.5){ resolve(data); }else{ reject("Thất bại"); } }, 3000) }) } // Thực thi hàm fetchData('https://dynonguyen.com') .then(function(data){ console.log(data) }) .catch(function(error){ console.log(error) }) .finally(function(){ console.log('Done') })
Điểm nổi bật của Promise
- Trong
.then()
bạn có thể tiếp tục trả về một Promise. Và kết quả của nó sẽ được đưa vào.then()
tiếp theo. VD: bạn gọi hàm fetchData() sau đó lấy dữ liệu vừa lấy được để gọi tiếp hàm fetchData khác. - Bạn có thể có rất nhiều
.then()
nhưng chỉ cần 1.catch()
duy nhất để bắt lỗi.
Nhờ đặc điểm trên mà chúng ta có thể giải quyết được vấn đề callback hell 😮
Async/Await trong Javascript
Từ khoá Async ra đời giúp chúng ta viết Promise gọn hơn. Ngoài ra, nếu dùng async thì chúng ta có thể dùng một từ khoá khác là await.
Cơ chế hoạt động:
Khi khai báo một hàm với từ khoá là async
thì mặc định hàm đó sẽ luôn trả về một Promise. Nếu bạn return
thì những gì được return sẽ được đưa vào Promise.resolve
. Còn khi bạn throw exception
thì nó sẽ được đưa vào Promise.reject
Từ đó, bạn hoàn toàn có thể sử dụng .then
và .catch
như với Promise. Lưu ý: Async vẫn hoạt động trên cơ chế của Promise, vì thế đừng hiểu nhầm có async thì không cần dùng Promise nữa 😅
// Định nghĩa một hàm fake lấy dữ liệu bất đồng bộ dùng Async async function fetchData(url){ const data = Math.random(); if(data > 0.5){ return data; }else{ throw "Thất bại"; } } // Thực thi hàm fetchData('https://dynonguyen.com') .then(function(data){ console.log(data) }) .catch(function(error){ console.log(error) }) .finally(function(){ console.log('Done') })
Từ khoá await:
Bên trong một async function, bạn có thể sử dụng từ khoá await. Với await nó sẽ đợi một hàm có trả về Promise hoàn thành (thành công hoặc thất bại) và sau đó mới thực thi tiếp các câu lệnh dưới nó. và nó sẽ trả giá trị trực tiếp của hàm đó luôn mà không cần phải .then()
// Sử dụng Promise function getData(url, param){ let result = null; callAPIs(url, param) .then(data1 => callAPIs(url, data2)) .then(data2 => callAPIs(url, data2)) .then(data3 => result = data3) .catch(err => console.error(err)); return result; } // Sử dụng async/await async function getData(url, param){ try{ const data1 = await callAPIs(url, param); // đợi data1 const data2 = await callAPIs(url, data1); // đợi data2 const data3 = await callAPIs(url, data2); return data3; }catch(error){ console.error(error); } }
Chú ý: await không dừng để đợi bên trong forEach, map
. Tốt nhất với trường hợp bạn nên dùng vòng loop bình thường hoặc for await
Promise.all()
Trong trường hợp ở một hàm nào đó, chúng ta phải call nhiều API giống ví dụ trên, nhưng những API này không ảnh hưởng đến nhau. Tức là việc call API thứ 2 không cần phải lấy dữ liệu của việc call API thứ nhất, …
Nếu chúng ta làm như ví dụ trên thì hàm này chỉ hoàn thành khi lần call API thứ 3 hoàn thành (hoặc xảy ra lỗi). Điều này sẽ không hợp lý trong case hiện tại vì các lần call API không cần phải đợi lẫn nhau.
Promise.all
sẽ giải quyết vấn đề này. Xem ví dụ sau:
// Hàm này sẽ mất 5s để hoàn thành nếu không có error xảy ra async function getData(url){ try{ const data1 = await callAPIs(url, 1); // mất 1s const data2 = await callAPIs(url, 2); // mất 3s const data3 = await callAPIs(url, 3); // mất 1s return data3; }catch(error){ console.error(error); } } // Hàm này sẽ mất 3s để hoàn thành nếu không có error xảy ra async function getData2(url){ try{ const data1 = callAPIs(url); // mất 1s const data2 = callAPIs(url); // mất 3s const data3 = callAPIs(url); // mất 1s return await Promise.all([data1, data2, data3]); }catch(error){ console.error(error); } }
Promise.all hoàn thành khi Promise chạy lâu nhất hoàn thành.
Tạm kết
Đó là những gì mình biết về Callback, Promise cũng như Async/Await trong Javascript. Chỉ có một cách để hiểu và vận dụng tốt những thứ này là luyện tập thật nhiều và gặp các trường hợp cụ thể. Lúc đấy, chúng ta mới có cái nhìn sâu hơn về các khái niệm loằng nhoằng này trong Javascript.
Cảm ơn mọi người đã đọc bài viết 😍
Tham khảo:
- Async function – Mozilla
- Javascript Promise – Web Dev Simplified
Bạn có thể tham khảo thêm:
- React Hook Form – Giải pháp quản lý form tuyệt với cho ReactJS
- Tuỳ chỉnh Windows terminal siêu xịn xò
- Tổng hợp website hữu ích cho dân lập trình web