Lập trình · 19/10/2026

Promise.finally trong JavaScript: dọn dẹp xong chưa chắc giữ được lỗi gốc

Tác vụ chính thất bại, nhưng màn hình chỉ báo lỗi từ bước dọn dẹp. Một nguyên nhân có thể nằm trong finally: callback này thường dùng để kết thúc trạng thái loading hoặc giải phóng tài nguyên, nhưng nếu chính nó ném lỗi thì kết quả của chuỗi Promise cũng thay đổi.

Promise.finally trong JavaScript: dọn dẹp xong chưa chắc giữ được lỗi gốc

Tác vụ chính thất bại, nhưng màn hình chỉ báo lỗi từ bước dọn dẹp. Một nguyên nhân có thể nằm trong finally: callback này thường dùng để kết thúc trạng thái loading hoặc giải phóng tài nguyên, nhưng nếu chính nó ném lỗi thì kết quả của chuỗi Promise cũng thay đổi.

Các đoạn code dưới đây chạy độc lập trong async function hoặc module hỗ trợ top-level await. Chúng dùng Promise local, không gọi API hay mở tài nguyên hệ thống.

1. Giá trị return thông thường không thay kết quả gốc

const result = await Promise.resolve('saved')
  .finally(() => 'cleanup finished');

console.log(result); // saved

finally không phải nơi biến đổi giá trị thành công như then. Callback cũng không nhận giá trị thành công hoặc lý do thất bại làm tham số. Nếu cần đọc dữ liệu kết quả, hãy làm ở then hoặc sau await.

Điều này hữu ích khi một thao tác cleanup phải chạy ở cả hai nhánh nhưng không cần biết nghiệp vụ đã trả dữ liệu gì. Việc return một chuỗi “fallback” trong finally không biến nó thành giá trị fallback của tác vụ.

2. Cleanup thành công giữ nguyên lý do thất bại

const original = new Error('save failed');
try {
  await Promise.reject(original)
    .finally(() => 'cleanup finished');
} catch (error) {
  console.log(error === original); // true
  console.log(error.message);      // save failed
}

finally không tự xử lý rejection. Bên gọi vẫn phải await trong try/catch, return chuỗi Promise cho tầng trên hoặc gắn catch có chủ đích. Chỉ thêm finally rồi bỏ qua Promise được trả về có thể để lại lỗi không được xử lý.

3. Nếu cleanup ném lỗi, lỗi đó thay thế kết quả trước

try {
  await Promise.reject(new Error('save failed'))
    .finally(() => {
      throw new Error('cleanup failed');
    });
} catch (error) {
  console.log(error.message); // cleanup failed
}

Hiện tượng tương tự xảy ra nếu callback trả Promise bị reject. Ngay cả khi tác vụ chính thành công, cleanup thất bại vẫn có thể khiến Promise cuối reject.

Vì thế cần phân loại cleanup: một bước bắt buộc hoàn tất hay chỉ là ghi nhận phụ trợ? Với lỗi phụ trợ được phép bỏ qua theo thiết kế, phải ghi nhận nó bằng cơ chế phù hợp và giữ nguyên lỗi chính có chủ đích. Với bước bắt buộc, có thể cần báo cả hai lỗi. Không đặt một catch rỗng chung để mọi cleanup thất bại đều biến mất.

4. Return hoặc await phần dọn dẹp bất đồng bộ

const events = [];
const result = await Promise.resolve('saved')
  .finally(async () => {
    await Promise.resolve();
    events.push('cleanup');
  });
events.push('after');

console.log(result);           // saved
console.log(events.join(',')); // cleanup,after

Callback async trả Promise nên chuỗi đợi cleanup trước khi đi tiếp. Nếu chỉ gọi một hàm bất đồng bộ trong callback thường mà không return Promise đó, chuỗi không có liên kết để chờ nó. Việc đặt await ở bên ngoài không tự nối những tác vụ đã bị tách ra.

Khi cleanup có thể chờ mãi, nó cũng có thể giữ Promise cuối ở trạng thái pending. Deadline hoặc cơ chế hủy, nếu cần, phải là một phần contract riêng; finally không tự cung cấp timeout.

5. Phân biệt finally của Promise và khối try/finally

Đây là hai cú pháp khác nhau. Một return trong khối finally của câu lệnh try có thể ghi đè luồng return hoặc throw của try. Trong khi đó, giá trị return thông thường từ callback Promise.finally bị bỏ qua như ví dụ đầu. Đừng chuyển quy tắc của cú pháp này sang cú pháp kia.

Với loading giao diện, cũng cần xét quyền sở hữu trạng thái: nhiều tác vụ chạy chồng có thể khiến tác vụ cũ tắt loading của tác vụ mới. Một finally chỉ biết Promise mà nó gắn vào, không tự quản lý toàn bộ tác vụ đang chạy của màn hình.

6. Kiểm thử cả lỗi chính lẫn lỗi cleanup

  • Tác vụ thành công, cleanup thành công: giữ giá trị gốc.
  • Tác vụ thất bại, cleanup thành công: giữ lý do thất bại gốc.
  • Cleanup ném lỗi đồng bộ hoặc reject bất đồng bộ: kiểm tra lỗi cuối đúng chính sách.
  • Cleanup được chờ trước khi bên gọi tiếp tục.
  • Chuỗi Promise cuối được await, return hoặc bắt lỗi đầy đủ.
  • Với giao diện, kiểm tra tác vụ chồng nhau và component đã đóng.

Các đoạn code đã chạy bằng Node.js local, gồm kiểm tra bổ sung cleanup reject và thứ tự hoàn tất. Chưa kiểm thử giao diện hoặc tài nguyên thật. Tài liệu đối chiếu: Promise.prototype.finally.

Đặt cleanup ở finally giúp giảm lặp code, nhưng vẫn cần một chính sách lỗi rõ ràng. Hãy kiểm tra Promise cuối mà bên gọi nhận được, không chỉ việc callback cleanup đã chạy.

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.