
Callbacks: Chìa khóa xử lý bất đồng bộ trong Node.js cho GenZ
Chào các chiến thần code GenZ! Anh Creyt lại lên sóng với một chủ đề mà nghe thì có vẻ “học thuật” nhưng thực ra nó là “chân ái” của dân lập trình, đặc biệt là khi các em làm việc với Node.js: Callback Functions.
1. Callback Functions là gì? (Kiểu GenZ dễ hiểu)
Để dễ hình dung, các em hãy tưởng tượng thế này: em đang lướt TikTok, lướt Instagram, nhắn tin với crush, chơi game... Nói chung là làm ti tỉ thứ cùng lúc. Em không bao giờ ngồi đợi một việc hoàn thành xong rồi mới làm việc khác, đúng không? Kiểu như không ai ngồi nhìn nồi cơm sôi rồi mới đi rửa bát cả.
Trong lập trình, đặc biệt là với Node.js, mọi thứ cũng chạy theo kiểu “đa nhiệm” như vậy. Node.js nổi tiếng với khả năng xử lý bất đồng bộ (asynchronous). Tức là, thay vì đợi một tác vụ tốn thời gian (như đọc file, gọi API, truy vấn database) hoàn thành xong thì mới làm việc khác, Node.js sẽ cứ thế mà chạy tiếp các tác vụ còn lại. Khi tác vụ tốn thời gian kia xong, nó sẽ “gọi lại” cho em biết kết quả.
À ha! Cái hành động “gọi lại” đó chính là Callback Function đấy các em. Hiểu đơn giản, một Callback Function là một hàm mà em truyền vào làm đối số cho một hàm khác, và cái hàm khác đó sẽ gọi lại nó (execute it) khi một tác vụ cụ thể hoàn thành.
Metaphor của Creyt: Tưởng tượng em nhờ đứa bạn thân đi mua trà sữa. Em không đứng đấy đợi nó mua rồi về mới làm việc khác. Em cứ việc học bài, chơi game, lướt web. Khi nào nó mua xong, nó sẽ "gọi điện" (callback) cho em, "Ê, trà sữa đây, ra lấy đi mày!" Cái cuộc gọi điện thoại đó chính là callback. Em đưa nó số điện thoại của em (hàm callback), nó gọi lại cho em khi xong việc.
2. Code Ví Dụ Minh Họa (Node.js)
Để các em không bị “ngáo chữ”, chúng ta cùng xem code minh họa nhé. Trong Node.js, các em sẽ gặp callback rất nhiều trong các module built-in như fs (File System) hay khi làm việc với server HTTP.
Ví dụ 1: setTimeout - Callback cơ bản nhất
setTimeout là một hàm global trong JavaScript/Node.js giúp thực thi một hàm sau một khoảng thời gian nhất định. Cái hàm được thực thi sau đó chính là callback.
console.log('Bắt đầu công việc.');
setTimeout(() => {
console.log('Công việc này hoàn thành sau 2 giây. (Đây là callback!)');
}, 2000);
console.log('Tiếp tục làm việc khác trong khi chờ đợi...');
// Output sẽ là:
// Bắt đầu công việc.
// Tiếp tục làm việc khác trong khi chờ đợi...
// (Sau 2 giây)
// Công việc này hoàn thành sau 2 giây. (Đây là callback!)
Các em thấy không? Dòng console.log('Tiếp tục làm việc khác...') chạy ngay lập tức mà không đợi setTimeout hoàn thành. Đó chính là bản chất bất đồng bộ!
Ví dụ 2: Đọc file với fs.readFile (Callback thực tế hơn)
Khi đọc một file, việc này có thể mất một ít thời gian. Node.js không muốn ứng dụng của em bị đứng hình để chờ đọc xong file. Thay vào đó, nó dùng callback.
Giả sử em có một file tên là data.txt với nội dung Hello Creyt's GenZ!. Em muốn đọc nội dung file này.
const fs = require('fs'); // Import module File System
console.log('1. Bắt đầu đọc file...');
fs.readFile('data.txt', 'utf8', (err, data) => {
// Hàm này là callback. Nó sẽ được gọi khi đọc file xong.
// 'err' sẽ chứa lỗi nếu có, 'data' sẽ chứa nội dung file.
if (err) {
console.error('Lỗi rồi mày ơi:', err);
return;
}
console.log('3. Đọc file thành công! Nội dung là:', data);
});
console.log('2. Đang làm việc khác trong khi chờ file được đọc...');
// Output sẽ tương tự:
// 1. Bắt đầu đọc file...
// 2. Đang làm việc khác trong khi chờ file được đọc...
// 3. Đọc file thành công! Nội dung là: Hello Creyt's GenZ!
Ở đây, hàm (err, data) => { ... } chính là callback. Nó được truyền vào fs.readFile. Node.js sẽ bắt đầu đọc file, và trong khi chờ đợi, nó sẽ chạy dòng console.log('2. Đang làm việc khác...'). Khi đọc file xong (hoặc có lỗi), Node.js mới gọi callback này để xử lý kết quả.

3. Mẹo và Best Practices của Creyt
-
Nguyên tắc "Error-first callback": Các em để ý trong ví dụ
fs.readFile, callback có dạng(err, data) => { ... }. Luôn luôn xử lýerr(lỗi) trước. Đây là một quy ước rất quan trọng trong Node.js. Nếuerrcó giá trị, nghĩa là có lỗi xảy ra, và các em nênreturnngay sau khi xử lý lỗi để tránh chạy tiếp code không mong muốn.
-
Đừng để rơi vào "Callback Hell" (Pyramid of Doom): Callback mạnh mẽ thật, nhưng khi các em có nhiều tác vụ bất đồng bộ phụ thuộc vào nhau, lồng ghép nhiều callback vào nhau sẽ tạo ra một cấu trúc code thụt vào sâu như kim tự tháp, rất khó đọc và bảo trì. Đây gọi là Callback Hell.
// Ví dụ Callback Hell (tránh nó nhé!) fs.readFile('file1.txt', (err, data1) => { if (err) return; fs.readFile('file2.txt', (err, data2) => { if (err) return; db.query('SELECT * FROM users', (err, users) => { if (err) return; // ... và cứ thế lồng vào nhau }); }); });Để giải quyết vấn đề này, JavaScript hiện đại đã có Promises và Async/Await, giúp code bất đồng bộ trở nên "phẳng" và dễ đọc hơn rất nhiều. Coi như Callback là nền móng, còn Promises/Async-Await là những tòa nhà chọc trời được xây trên nền móng đó vậy.
-
Giữ callback đơn giản: Mỗi callback chỉ nên làm một việc cụ thể. Nếu nó quá phức tạp, hãy tách nó ra thành các hàm nhỏ hơn.
-
Đặt tên biến rõ ràng:
errcho lỗi,datahoặcresultcho kết quả. Điều này giúp code dễ đọc hơn.
4. Góc học thuật Harvard (dễ hiểu tuyệt đối)
Từ góc độ học thuật, callback là một ví dụ điển hình của Higher-Order Functions – các hàm có thể nhận các hàm khác làm đối số hoặc trả về một hàm. Trong JavaScript, các hàm được coi là "first-class citizens" (công dân hạng nhất), nghĩa là chúng có thể được gán cho biến, truyền làm đối số, và trả về từ các hàm khác giống như bất kỳ kiểu dữ liệu nào (số, chuỗi, object).
Cơ chế hoạt động của callback gắn liền với Event Loop của Node.js. Khi một tác vụ bất đồng bộ được khởi tạo, nó sẽ được đẩy sang một "side-thread" hoặc được xử lý bởi hệ điều hành. Node.js Event Loop tiếp tục xử lý các tác vụ khác trong hàng đợi chính. Khi tác vụ bất đồng bộ hoàn thành, kết quả của nó (cùng với callback tương ứng) được đẩy vào một hàng đợi khác (ví dụ: callback queue), và Event Loop sẽ lấy callback đó ra và thực thi nó khi stack gọi chính rảnh rỗi. Đây là cách Node.js duy trì khả năng non-blocking I/O (input/output không chặn) hiệu quả.
Callbacks giúp chúng ta đạt được Separation of Concerns (tách biệt các mối quan tâm) bằng cách định nghĩa rõ ràng logic xử lý kết quả sau khi một tác vụ hoàn thành, mà không làm rối code chính đang khởi tạo tác vụ đó.
5. Ví dụ thực tế các ứng dụng/website đã ứng dụng
Callbacks được ứng dụng NHIỀU ĐẾN MỨC mà các em không hề hay biết:
- Web Servers (Express.js): Khi các em định nghĩa một route trong Express.js, các em thường viết
app.get('/api/users', (req, res) => { ... });. Cái(req, res) => { ... }chính là một callback function, nó sẽ được gọi khi có request HTTP đến đường dẫn/api/users. - Database Interactions: Hầu hết các thư viện tương tác với database (như MongoDB driver, MySQL driver) trong Node.js đều sử dụng callback để xử lý kết quả truy vấn bất đồng bộ.
// Ví dụ với MongoDB (thường dùng Promises/Async-Await hơn, nhưng callback vẫn là nền tảng) db.collection('users').findOne({ name: 'Creyt' }, (err, user) => { if (err) throw err; console.log('Tìm thấy user:', user); }); - API Calls: Khi các em dùng thư viện như
axioshoặcnode-fetch(dù chúng thường trả về Promises), về bản chất, chúng cũng đang xử lý các sự kiện mạng bất đồng bộ và trả về kết quả thông qua một cơ chế tương tự callback. - File Uploads: Khi người dùng upload file lên server Node.js, quá trình xử lý file (lưu trữ, đổi tên...) thường được thực hiện bất đồng bộ với callback.
6. Thử nghiệm đã từng và hướng dẫn nên dùng cho case nào
Anh Creyt đã từng "ăn ngủ" với callback trong những ngày đầu làm việc với Node.js. Hồi đó, fs.readFile, http.createServer, hay các thư viện database đều dùng callback "nguyên chất". Anh còn nhớ có lần phải đọc 3 file liên tiếp, mỗi file phụ thuộc vào kết quả của file trước, và kết thúc bằng việc lưu vào database. Kết quả là một cái Callback Hell "siêu to khổng lồ", code thụt vào đến nỗi phải kéo thanh cuộn ngang mới đọc được hết một dòng. Đó là một trải nghiệm nhớ đời!
Vậy nên dùng callback trong trường hợp nào?
- Tác vụ bất đồng bộ đơn giản: Khi em chỉ có một hoặc hai tác vụ bất đồng bộ không lồng ghép quá phức tạp. Ví dụ, ghi log, gửi email đơn giản.
- Event Handling: Callback rất phù hợp để xử lý các sự kiện (events). Ví dụ, khi một sự kiện
datahoặcendxảy ra trên một stream đọc file, em sẽ dùng callback để xử lý dữ liệu. - Code base cũ: Nếu em đang làm việc với một dự án Node.js "lão làng" được viết từ lâu, khả năng cao là họ vẫn đang dùng callback rất nhiều. Việc hiểu rõ callback là chìa khóa để bảo trì và mở rộng code đó.
Còn khi nào thì nên "say bye" với callback và dùng Promises/Async-Await?
- Khi có nhiều tác vụ bất đồng bộ phụ thuộc vào nhau: Để tránh Callback Hell, Promises với
.then().catch()hoặcasync/awaitlà lựa chọn tối ưu, giúp code dễ đọc và quản lý lỗi tốt hơn rất nhiều. - Khi muốn xử lý lỗi tập trung: Promises và Async/Await cung cấp cơ chế xử lý lỗi mạnh mẽ hơn (ví dụ:
.catch()hoặctry/catch).
Tóm lại: Callback là nền tảng, là "ông tổ bà cô" của lập trình bất đồng bộ trong JavaScript/Node.js. Hiểu rõ nó là bước đệm vững chắc để các em chinh phục những khái niệm nâng cao hơn như Promises và Async/Await. Hãy coi nó như một người bạn cũ, không phải lúc nào cũng gặp nhưng khi cần thì vẫn luôn ở đó để giúp đỡ.
Chúc các em code mượt mà, không bug!
Thuộc Series: Nodejs
Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!