Tổng kết cú pháp — Bảng tra nhanh phương thức mảng, bất đồng bộ và ngoại lệ

Các bảng tra nhanh: chọn phương thức mảng theo giá trị trả về, so sánh callback, then và await, và nơi mỗi loại lỗi được bắt.

Cộng tổng một danh sách hóa đơn, chờ upload xong, bắt lỗi rồi hiện thông báo — mỗi việc này đều có vài cách viết. Nếu chọn sai, bạn có thể nhận về undefined ở chỗ đang chờ một mảng, hoặc gặp một lỗi không đến được catch nào cả.

Bài này tổng hợp khi nào dùng phương thức mảng nào, chờ xử lý bất đồng bộ theo cách nào, và bắt ngoại lệ ra sao.

Chọn theo kết quả bạn muốn — Phương thức mảng trả về gì

Giả sử từ một danh sách hóa đơn, bạn muốn biết ba điều: có bao nhiêu hóa đơn đã thanh toán, hóa đơn chưa thanh toán đầu tiên, và tổng số tiền. Các phương thức mảng đều được gọi theo cùng một kiểu, kèm một callback, nên chỉ nhìn lời gọi thì bạn không biết sẽ nhận về một mảng hay một giá trị đơn lẻ.

Ở đây, các phương thức được chia nhóm theo dạng giá trị trả về: một mảng mới, một trong các phần tử vốn có trong mảng, hoặc một giá trị tính từ mảng, như boolean hay một tổng. Biết được dạng đó, bạn cũng biết mình có thể gọi join trên kết quả hay đọc property như .id từ nó.

const invoices = [
  { id: "B-301", amount: 4800, paid: true },
  { id: "B-302", amount: 1200, paid: false },
  { id: "B-303", amount: 3600, paid: true },
  { id: "B-304", amount: 2500, paid: false },
];

// Trả về mảng: map giữ nguyên số phần tử, filter chỉ giữ các phần tử thỏa điều kiện
console.log(invoices.map((invoice) => invoice.id).join(", "));   // B-301, B-302, B-303, B-304
console.log(invoices.filter((invoice) => invoice.paid).length);  // 2

// Trả về chính phần tử: phần tử đầu tiên thỏa điều kiện
console.log(invoices.find((invoice) => !invoice.paid).id);  // B-302

// Trả về giá trị tính từ mảng: some cho boolean, reduce cho giá trị đã gộp
console.log(invoices.some((invoice) => invoice.amount >= 4000));              // true
console.log(invoices.reduce((total, invoice) => total + invoice.amount, 0));  // 12100
Cùng một mảng, khác dạng giá trị trả về
invoices4 hóa đơn.filterkiểm tra paid.findkiểm tra !paid.reducecộng amountMảng (2 phần tử)B-301 và B-303Một phần tửhóa đơn B-302Một số12100Tiếp theo.map hoặc .joinTiếp theo.id hoặc .amountĐưa thẳng vàoconsole.log
Kết quả là một mảng, một phần tử hay một số, và điều đó quyết định bạn viết được gì phía sau. Biết mình cần kết quả dạng nào thì sẽ thu hẹp được phương thức cần chọn.

Nếu dùng filter khi chỉ cần một phần tử, bạn nhận về một mảng chứa một phần tử, nên phải thêm [0] rồi mới đọc được .id. Bảng dưới đây liệt kê các phương thức mảng nhận callback đã học trong khóa này, chia nhóm theo giá trị trả về.

Phương thứcTrả vềCần lưu ý
mapMảng mới (cùng số phần tử)Mảng gốc không thay đổi
filterMảng mới (chỉ phần tử thỏa)Mảng rỗng nếu không phần tử nào thỏa
find / findIndexMột phần tử / một indexundefined / -1 nếu không tìm thấy
some / everyBooleanfalse / true với mảng rỗng
reduceGiá trị lần gọi cuối trả vềMảng rỗng thì cần giá trị khởi tạo
sort / toSortedMảng đã sắp xếpsort thay đổi cả mảng gốc

Khi nào nối tiếp được — Method chaining và forEach

Giả sử bạn chỉ muốn tính tổng các hóa đơn chưa thanh toán. Nếu lưu kết quả của filter vào một biến, gọi map trên biến đó rồi mới gọi reduce, bạn phải đặt tên cho những mảng trung gian không bao giờ dùng lại. Còn nếu cố viết tất cả trong một biểu thức mà không biết phương thức nào nối sau được phương thức nào, bạn sẽ gặp TypeError ở chỗ hai phương thức nối vào nhau.

Trong method chaining (gọi phương thức tiếp theo trên giá trị trả về của phương thức trước), mỗi lời gọi như .filter(...) được tính là một bước, và mỗi bước được gọi trên thứ mà bước trước nó trả về. Bạn chỉ gọi tiếp được phương thức mảng khi bước trước trả về một mảng.

// invoices là 4 hóa đơn giống phần trước

// Chừng nào còn nhận về mảng, bạn còn nối thêm được phương thức mảng
const unpaidTotal = invoices
  .filter((invoice) => !invoice.paid)             // Mảng: 2 hóa đơn B-302 và B-304
  .map((invoice) => invoice.amount)               // Mảng: [1200, 2500]
  .reduce((total, amount) => total + amount, 0);  // Số: 3700
console.log(unpaidTotal);                         // 3700

// toSorted cũng trả về mảng, nên nối tiếp được sang map và join
const idsByAmount = invoices.toSorted((a, b) => b.amount - a.amount).map((invoice) => invoice.id);
console.log(idsByAmount.join(", "));  // B-301, B-303, B-304, B-302

// forEach trả về undefined, nên nối gì phía sau cũng bị lỗi
invoices.forEach((invoice) => console.log(invoice.id)).map((invoice) => invoice.id);
// Sau khi in ra B-301 đến B-304
// TypeError: Cannot read properties of undefined (reading 'map')
Chỉ nối tiếp được khi còn nhận về mảng
invoicesmảng 4 phần tử.filter(...)mảng 2 phần tử.map(...)[1200, 2500].reduce(...)3700invoicesmảng 4 phần tử.forEach(...)undefined.map(...)trên undefinedBáo lỗiTypeError
Ở hàng trên, mỗi bước đều trả về mảng cho đến khi reduce trả về một số. forEach trả về undefined, nên chuỗi bị đứt ở bước tiếp theo.

Hãy đặt những bước không trả về mảng ở cuối chuỗi. Sau find, bạn đọc được property như .id nhưng không gọi được map, còn số mà reduce trả về thì cứ lưu vào biến để dùng.

Gọi map sau find sẽ ra thông báo khác

Nếu nối .map(...) vào phần tử mà find trả về hay số mà reduce trả về, chương trình sẽ dừng lại và báo TypeError, chẳng hạn invoices.find(...).map is not a function. Thông báo này khác với thông báo sau forEach, nhưng trong cả hai trường hợp, bước ngay trước .map đều không trả về mảng.

So sánh các cách chờ — Callback / then / await

Giả sử bạn upload một bức ảnh rồi tạo thumbnail cho nó. Cả hai việc đều mất thời gian mới có kết quả, nên hàm không thể trả kết quả ngay qua giá trị trả về, và bước thứ hai phải chờ đến khi có kết quả của bước thứ nhất.

Trong code dưới đây, ① dùng kiểu callback (bạn truyền cho hàm bắt đầu công việc một hàm khác, hàm này sẽ được gọi khi công việc xong; không có Promise nào được trả về), cũng là cách setTimeout hoạt động. ② truyền một hàm cho then của Promise nhận được, còn ③ dùng await bên trong hàm async.

// ① Kiểu callback: truyền hàm sẽ được gọi khi xong, và viết bước tiếp theo bên trong nó
// later gọi callback với value sau 100ms (không trả về Promise)
function later(value, callback) {
  setTimeout(() => callback(value), 100);
}
later("photo-17.jpg", (fileName) => {
  later(`thumbnail của ${fileName}`, (thumbnail) => {
    console.log(`① Đã tạo ${thumbnail}`);  // ① Đã tạo thumbnail của photo-17.jpg
  });
});

// ② then: nối bước tiếp theo bằng .then (delay giống bài về Promise)
delay(100, "photo-17.jpg")
  .then((fileName) => delay(100, `thumbnail của ${fileName}`))
  .then((thumbnail) => console.log(`② Đã tạo ${thumbnail}`));  // ② Đã tạo thumbnail của photo-17.jpg

// ③ await: viết bước tiếp theo ở dòng dưới và nhận kết quả vào biến (giả định chạy bên trong hàm async, như bài trước)
const fileName = await delay(100, "photo-17.jpg");
const thumbnail = await delay(100, `thumbnail của ${fileName}`);
console.log(`③ Đã tạo ${thumbnail}`);  // ③ Đã tạo thumbnail của photo-17.jpg
Bước tiếp theo bị lồng sâu đến đâu
① Kiểu callback — truyền một hàm cho later
  • Gọi later("photo-17.jpg", (fileName) => { … })
Bên trong (fileName) => { … }
  • Kết quả thứ nhất được đưa vào fileName
  • Gọi later thứ hai ở đây
Bên trong (thumbnail) => { … }
  • Kết quả thứ hai được đưa vào thumbnail
  • Phần in ra nằm trong hàm trong cùng
② then — truyền một hàm cho .then
  • Bắt đầu từ delay(100, "photo-17.jpg")
Bên trong hàm truyền cho .then thứ nhất
  • Nhận fileName và trả về delay tiếp theo
Bên trong hàm truyền cho .then thứ hai
  • Nhận thumbnail và in ra
③ await — nhận kết quả vào biến
  • const fileName = await delay(…)
  • const thumbnail = await delay(…)
  • Viết console.log(…) ở dòng tiếp theo
Chỉ với kiểu callback ở ①, hàm tiếp theo mới phải nằm trong hàm nhận kết quả. await ở ③ không tạo hàm nào; các bước chỉ là những dòng viết từ trên xuống.

Hàm trong then thứ hai chỉ nhận thumbnail, nên muốn dùng cả fileName ở đó, bạn phải chép nó ra một biến bên ngoài. Với await, fileName nằm trong một biến mà mọi dòng bên dưới đều đọc được. Bảng dưới đây đặt ba cách viết cạnh nhau.

Cách viếtKết quả được đưa vào đâuKhi thêm bước
Kiểu callbackTham số của hàm bạn đã truyềnMỗi bước lồng sâu thêm một cấp
Truyền hàm cho thenTham số của hàm truyền cho thenCác bước nối tiếp ở cùng một cấp
Chờ bằng awaitBiến ở vế tráiMỗi bước viết ở dòng tiếp theo

Kiểm tra lỗi rơi vào đâu — try / catch và .catch

Giả sử hàm giữ hàng trong kho sẽ thất bại khi số lượng đặt vượt quá số hàng còn trong kho. Với hàm bình thường không trả về Promise, bạn bắt được lỗi bằng cách bọc dòng gọi hàm trong try / catch, nhưng lỗi từ hàm trả về Promise thì có đến được khối catch hay không còn tùy vào cách bạn viết lời gọi.

Khối catch đi sau try nhận các ngoại lệ được ném ra từ những dòng nằm trong try. .catch của Promise thì gọi hàm bạn truyền vào khi Promise đó bị rejected.

function checkStock(count) {
  if (count > 3) {
    throw new Error(`Không đủ hàng trong kho: ${count} sản phẩm`);
  }
}
async function reserveStock(count) {
  await delay(100);   // delay giống bài về Promise
  checkStock(count);  // throw ở đây thì Promise mà hàm này trả về bị rejected
}

// ① Hàm không trả về Promise: bọc dòng gọi hàm trong try
try { checkStock(5); } catch (error) { console.log(`① ${error.message}`); }
// ② Cách viết then: truyền cho .catch một hàm để nhận lỗi
reserveStock(5).catch((error) => console.log(`② ${error.message}`));
// ③ Cách viết await: bọc dòng await trong try
try { await reserveStock(5); } catch (error) { console.log(`③ ${error.message}`); }

// ① Không đủ hàng trong kho: 5 sản phẩm
// ② Không đủ hàng trong kho: 5 sản phẩm (sau 100ms)
// ③ Không đủ hàng trong kho: 5 sản phẩm (sau 100ms)
Ba nơi nhận cùng một lỗi
checkStock throwkhi quá 3 sản phẩm① checkStock(5)gọi trực tiếp② reserveStock(5).catch(...)③ awaitreserveStock(5)Dòng gọi hàmném ngoại lệPromise trả vềbị rejectedDòng awaitném ngoại lệKhối catchsau tryTham số củahàm trong .catchKhối catchsau try
Ở ②, dòng gọi hàm không ném ngoại lệ; thay vào đó, Promise được trả về bị rejected. Khối catch chỉ nhận được ① và ③, nơi chính dòng gọi ném ra ngoại lệ.

Dù nhận bằng .catch hay khối catch, bạn đều nhận được cùng một Error, nên error.message và phép kiểm tra instanceof đều dùng được như nhau. Bảng dưới đây tổng hợp các cách xử lý ngoại lệ đã học trong khóa này.

Cách viếtTác dụngCần lưu ý
try / catchNhận ngoại lệ trong trycatch để trống sẽ che mất lỗi
finallyLuôn chạy sau cùngĐặt phần dọn dẹp ở đây
throw new ErrorNém ngoại lệ ngay tại chỗ đóKhông throw thì không dừng
class extends ErrorLoại lỗi phân biệt bằng instanceofKiểm tra lớp tự tạo trước
cause của ErrorĐính kèm lỗi gốcĐọc bằng error.cause
.catch của PromiseĐược gọi khi Promise bị rejectedThêm vào cuối chuỗi then
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1invoices.filter((invoice) => invoice.id === "B-302") trả về gì?

Câu 2Trong invoices.filter(...).map(...).reduce(...), reduce gộp mảng nào?

Câu 3Khi nối ba bước bất đồng bộ theo kiểu callback, bạn viết bước thứ ba ở đâu?