Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn đang vận hành một quy trình "Xử lý đơn hàng online" tại một sàn thương mại điện tử. Mọi thứ chạy mượt mà: khách đặt hàng, hệ thống trừ tiền, kho đóng gói, shipper giao hàng. Nhưng rồi một ngày, cổng thanh toán của ngân hàng bị treo đúng lúc khách bấm "Thanh toán". Tiền đã bị trừ một phần, đơn hàng đã được tạo, nhưng kho chưa nhận lệnh đóng gói. Quy trình của bạn đang ở trạng thái "lửng lơ" — không hoàn thành, không thất bại rõ ràng. Khách hàng gọi điện phàn nàn, nhân viên CSKH không biết đơn đang ở đâu trong luồng.
Đây chính là lý do Error Handling (xử lý lỗi) tồn tại trong BPMN. Một quy trình tốt không phải là quy trình giả định "mọi thứ luôn suôn sẻ" (cái mà người ta gọi là happy path — luồng hạnh phúc). Một quy trình trưởng thành phải biết: khi có sự cố xảy ra ở giữa chừng thì ai phát hiện ra, làm gì tiếp theo, và làm sao để quy trình tự phục hồi hoặc chuyển sang một nhánh xử lý khác một cách có kiểm soát.
Trong BPMN 2.0, cơ chế cốt lõi để làm việc này là cặp đôi Throw Error (ném lỗi) và Catch Error (bắt lỗi). Đây không phải là khái niệm trừu tượng dành cho lập trình viên — nó là cách mô hình hóa chính xác câu chuyện "có chuyện xảy ra ở chỗ này, phải báo lên chỗ kia để xử lý". Nắm vững nó, bản đồ quy trình của bạn sẽ thể hiện được cả mặt sáng lẫn mặt tối của thực tế vận hành, và đặc biệt quan trọng nếu sau này bạn đưa quy trình lên engine tự động hóa như Camunda.
Khái niệm cốt lõi
Error là gì trong BPMN
Trong BPMN, Error là một loại sự kiện đặc biệt biểu diễn một điều kiện bất thường có tên (named error) xảy ra trong quá trình thực thi và làm gián đoạn luồng bình thường. Khác với một quyết định rẽ nhánh thông thường (mà bạn dùng Gateway để xử lý — đã học ở Bài 15, 16), Error đại diện cho tình huống "không thể tiếp tục theo cách thông thường được nữa, cần thoát ra và chuyển giao cho người/luồng có khả năng xử lý".
Điểm mấu chốt cần nhớ ngay: Error trong BPMN luôn là interrupting (gây gián đoạn). Khi một lỗi được ném ra, hoạt động hoặc sub-process đang chạy sẽ bị chấm dứt ngay lập tức, token thoát ra và đi theo luồng xử lý lỗi. Điều này khác với một số sự kiện khác (như Message hay Signal) có thể là non-interrupting. Lỗi, theo bản chất, không cho phép "vừa lỗi vừa tiếp tục chạy bình thường".
Throw Error — ném lỗi ra
Throw Error là hành động phát ra một lỗi. Có hai vị trí điển hình:
1. End Error Event trong sub-process. Đây là tình huống phổ biến nhất. Bên trong một sub-process, khi luồng đi tới một nhánh không thể hoàn thành được mục tiêu, bạn đặt một End Error Event (sự kiện kết thúc kiểu lỗi — vòng tròn viền đậm có biểu tượng tam giác tô đen). Khi token chạm vào nó, sub-process không kết thúc bình thường mà "ném" lỗi đó ra ngoài cho process cha bắt. Ví dụ: trong sub-process "Thẩm định hồ sơ vay", nếu phát hiện khách thuộc danh sách nợ xấu CIC, bạn ném lỗi HoSoKhongDuDieuKien ra ngoài.
2. Service Task ném lỗi qua code. Khi quy trình được thực thi trên một engine (như Camunda), một Service Task gọi tới một đoạn code/dịch vụ. Nếu đoạn code đó gặp tình huống nghiệp vụ không xử lý được, nó có thể chủ động ném ra một BPMN Error với một mã lỗi (errorCode) cụ thể. Đây là cầu nối giữa lỗi kỹ thuật trong code và mô hình nghiệp vụ trong sơ đồ. Lưu ý quan trọng: ta phân biệt BPMN Error (lỗi nghiệp vụ có chủ đích, được mô hình hóa) với technical exception (lỗi kỹ thuật ngoài ý muốn như mất kết nối DB). Chỉ những lỗi nghiệp vụ mà bạn muốn quy trình phản ứng theo một cách cụ thể mới nên được ném dưới dạng BPMN Error.
Catch Error — bắt lỗi
Catch Error là phía nhận và xử lý lỗi. Có hai hình thức:
1. Error Boundary Event (sự kiện biên kiểu lỗi). Đây là cách hay dùng nhất. Bạn gắn một sự kiện hình lỗi vào biên (đường viền) của một Activity hoặc Sub-process. Khi hoạt động đó ném lỗi ra, boundary event sẽ bắt được và token chuyển sang luồng xử lý lỗi gắn với boundary đó. Vì Error luôn interrupting, hoạt động bị gắn boundary sẽ dừng lại hoàn toàn. Ở Bài 21 bạn đã học về boundary event nói chung; Error Boundary Event là một trường hợp đặc biệt của nó, và nó luôn có viền nét liền (interrupting), không bao giờ là nét đứt (non-interrupting).
2. Error Start Event trong Event Sub-process. Một cách tinh tế hơn: bên trong một process, bạn đặt một Event Sub-process với một Error Start Event. Khi lỗi xảy ra trong process cha, event sub-process này được kích hoạt để xử lý. Cách này gọn gàng hơn khi bạn không muốn vẽ boundary trên từng activity riêng lẻ.
Cơ chế khớp lỗi qua errorCode
Một câu hỏi quan trọng: làm sao engine biết lỗi nào được bắt bởi catch nào? Câu trả lời là errorCode (và thuộc tính Error được khai báo). Throw Error mang theo một mã lỗi; Catch Error khai báo nó "quan tâm" đến mã lỗi nào. Nếu mã khớp, lỗi được bắt. Nếu một Error Boundary Event để trống errorCode, nó sẽ bắt mọi lỗi ném ra từ hoạt động đó. Nguyên tắc lan truyền: lỗi sẽ "đi lên" (propagate) từ trong ra ngoài, từ sub-process con tới process cha, cho tới khi gặp một catcher khớp. Nếu không ai bắt, quy trình thất bại — và đó là điều bạn không muốn để xảy ra ngoài tầm kiểm soát.
Tình huống thực tế
Ví dụ 1 — Thanh toán đơn hàng tại Tiki
Hãy lấy bối cảnh sàn thương mại điện tử Tiki với quy trình "Xác nhận và thanh toán đơn hàng". Đội thiết kế quy trình mô hình hóa một sub-process tên "Xử lý thanh toán" gồm các bước: Kiểm tra tồn kho → Tạm giữ hàng → Gọi cổng thanh toán → Xác nhận giao dịch.
Vấn đề thực tế: trung bình mỗi ngày có khoảng 3-4% giao dịch bị từ chối bởi cổng thanh toán (thẻ hết hạn, không đủ số dư, hoặc nghi ngờ gian lận). Trong giờ cao điểm Flash Sale, con số này vọt lên gần 8%. Nếu không xử lý lỗi, hàng đã bị "tạm giữ" trong kho nhưng giao dịch thất bại — dẫn tới tồn kho ảo, khách khác không mua được dù hàng vẫn còn.
Cách họ giải quyết: bên trong sub-process "Xử lý thanh toán", Service Task "Gọi cổng thanh toán" được lập trình để ném BPMN Error với errorCode PAYMENT_DECLINED khi nhận phản hồi từ chối. Ở biên của sub-process, đội thiết kế gắn một Error Boundary Event bắt mã PAYMENT_DECLINED. Token thoát ra theo luồng này chạy tới một activity "Hoàn trả hàng tạm giữ về kho" rồi tới "Gửi email mời khách thử phương thức khác". Toàn bộ diễn ra tự động trong vài giây.
Bài học rút ra: Error Handling không chỉ để "báo lỗi" mà còn để dọn dẹp hệ quả của lỗi (ở đây là hoàn trả tồn kho tạm giữ). Việc tách riêng errorCode PAYMENT_DECLINED giúp họ xử lý nó khác hẳn với một lỗi khác như INVENTORY_NOT_AVAILABLE — mỗi loại có một luồng dọn dẹp riêng.
Ví dụ 2 — Thẩm định hồ sơ vay tại một công ty tài chính tiêu dùng
Một công ty tài chính tiêu dùng (kiểu FE Credit hay Home Credit) có quy trình "Phê duyệt khoản vay trả góp" cho khách mua điện thoại tại cửa hàng. Trong đó có sub-process "Thẩm định tự động" gồm: Kiểm tra CMND/CCCD → Tra cứu CIC → Chấm điểm tín dụng → Ra quyết định.
Thực tế: bước "Tra cứu CIC" phụ thuộc vào hệ thống của Trung tâm Thông tin Tín dụng Quốc gia. Có những thời điểm hệ thống CIC bảo trì hoặc trả về kết quả khách thuộc nhóm nợ xấu (nhóm 3, 4, 5). Đây là hai tình huống khác nhau và cần xử lý khác nhau.
Họ thiết kế: nếu khách thuộc nhóm nợ xấu, Service Task ném End Error Event trong sub-process với errorCode CREDIT_REJECTED. Process cha có Error Boundary Event bắt mã này, chuyển sang nhánh "Từ chối lịch sự và lưu hồ sơ vào danh sách theo dõi". Còn nếu hệ thống CIC không phản hồi (lỗi kỹ thuật), họ không dùng BPMN Error mà dùng một cơ chế khác (timer escalation — sẽ học ở Bài 42), vì đây không phải quyết định nghiệp vụ mà là sự cố hạ tầng cần con người can thiệp.
Con số minh họa: sau khi triển khai cách phân tách này, thời gian xử lý trung bình một hồ sơ giảm từ 12 phút xuống còn 4 phút, vì các hồ sơ bị từ chối được "thoát" sớm qua đường error thay vì chạy hết toàn bộ luồng.
Bài học rút ra: Phải phân biệt rạch ròi giữa lỗi nghiệp vụ (dùng BPMN Error) và lỗi kỹ thuật (xử lý bằng cơ chế khác). Đây là sai lầm kinh điển khiến nhiều sơ đồ trở nên rối loạn.
Ví dụ 3 — Quy trình duyệt nghỉ phép nội bộ tại một startup công nghệ
Một startup khoảng 80 nhân sự ở TP.HCM xây dựng quy trình "Đăng ký nghỉ phép" trên một nền tảng BPM low-code. Sub-process "Kiểm tra số ngày phép còn lại" sẽ ném BPMN Error INSUFFICIENT_LEAVE_BALANCE nếu nhân viên xin nghỉ vượt quá số phép năm còn lại.
Họ dùng Event Sub-process với Error Start Event thay vì boundary event, vì lỗi này có thể phát sinh từ nhiều bước khác nhau trong quy trình (cả lúc đăng ký lẫn lúc quản lý duyệt điều chỉnh ngày). Event sub-process bắt lỗi này và tự động chuyển hướng: gửi thông báo cho nhân viên kèm gợi ý chuyển sang "nghỉ không lương" và thông báo cho HR.
Bài học rút ra: Khi cùng một loại lỗi có thể phát sinh từ nhiều nơi trong quy trình, Event Sub-process với Error Start Event gọn hơn nhiều so với việc vẽ boundary event lặp lại trên từng activity. Chọn hình thức catch phù hợp với phạm vi phát sinh lỗi.
Hướng dẫn từng bước
Dưới đây là quy trình bạn nên theo khi thêm error handling vào một bản đồ quy trình:
Bước 1 — Xác định "điểm gãy" nghiệp vụ. Đi qua từng activity và tự hỏi: "Bước này có thể thất bại vì lý do nghiệp vụ nào?" (không tính lỗi kỹ thuật). Liệt kê ra. Ví dụ: thanh toán bị từ chối, hồ sơ không đủ điều kiện, hết hạn mức.
Bước 2 — Đặt tên và mã cho từng lỗi. Mỗi lỗi cần một Error được khai báo với name (tên dễ đọc) và errorCode (mã định danh, viết HOA_GẠCH_DƯỚI theo quy ước). Ví dụ: PAYMENT_DECLINED, CREDIT_REJECTED. Mã này là "hợp đồng" giữa throw và catch.
Bước 3 — Đặt Throw Error. Quyết định lỗi được ném ở đâu: nếu là kết thúc một nhánh trong sub-process, dùng End Error Event. Nếu là từ một dịch vụ/code trong Service Task, cấu hình task đó ném BPMN Error với đúng errorCode.
Bước 4 — Đặt Catch Error. Chọn một trong hai:
- Error Boundary Event gắn lên biên activity/sub-process khi lỗi cục bộ, chỉ phát sinh từ một hoạt động cụ thể.
- Error Start Event trong Event Sub-process khi lỗi có thể đến từ nhiều nơi hoặc bạn muốn gom logic xử lý lỗi vào một chỗ.
Bước 6 — Kiểm tra việc lan truyền. Đảm bảo mỗi lỗi ném ra đều có ít nhất một catcher khớp errorCode ở đâu đó phía ngoài. Nếu cố tình để một lỗi "đi lên tận cùng" để dừng cả quy trình, hãy ghi chú rõ ý định đó.
Lỗi thường gặp & mẹo
Lỗi 1 — Nhầm lỗi nghiệp vụ với lỗi kỹ thuật. Đây là sai lầm phổ biến nhất. "Mất kết nối database" hay "API timeout" không nên là BPMN Error. BPMN Error dành cho các điều kiện nghiệp vụ mà bạn muốn quy trình phản ứng theo kịch bản định trước. Lỗi kỹ thuật thường cần retry, timer escalation, hoặc cảnh báo cho đội vận hành.
Lỗi 2 — Quên rằng Error luôn interrupting. Một số học viên cố gắng vẽ Error Boundary Event với nét đứt (non-interrupting) như Message hay Timer boundary. Điều này sai về mặt chuẩn BPMN. Error theo bản chất luôn gây gián đoạn — hoạt động đang chạy phải dừng. Nếu bạn muốn "vừa cảnh báo vừa tiếp tục", thứ bạn cần không phải là Error.
Lỗi 3 — errorCode không khớp giữa throw và catch. Throw ném PAYMENT_DECLINED nhưng catch lại khai báo PAYMENT_FAILED — lỗi sẽ không được bắt và lan tới tận ngoài, có thể làm chết cả quy trình. Mẹo: lập một bảng đăng ký mã lỗi (error registry) dùng chung cho cả nhóm, tránh mỗi người gõ một kiểu.
Lỗi 4 — Bắt lỗi nhưng không dọn dẹp hệ quả. Bắt được PAYMENT_DECLINED nhưng quên hoàn trả hàng đã tạm giữ. Lỗi này âm thầm hơn — quy trình vẫn "chạy đúng" trên sơ đồ nhưng để lại rác trong dữ liệu thực. Luôn tự hỏi: "Lỗi này đã làm thay đổi trạng thái gì cần được undo không?" (Việc undo có chủ đích này liên quan tới Compensation — chủ đề riêng của Bài 23).
Mẹo — Đặt một boundary error trống để bắt lỗi không mong đợi. Trên các sub-process quan trọng, gắn thêm một Error Boundary Event để trống errorCode (bắt mọi lỗi) dẫn tới một luồng "ghi log và báo cho con người". Đây như một "lưới an toàn" cho những lỗi bạn chưa lường trước.
Mẹo — Dùng tên lỗi theo ngôn ngữ nghiệp vụ. CREDIT_REJECTED dễ đọc với cả nhân viên nghiệp vụ lẫn dev hơn là ERR_4032. Sơ đồ BPMN là để giao tiếp giữa người với người trước, máy sau.
Bài tập thực hành
Hãy vẽ một bản đồ quy trình BPMN cho tình huống sau (dùng draw.io, Bizagi hoặc Camunda Modeler — các công cụ sẽ học kỹ ở Bài 31-33):
Bối cảnh: Một chuỗi nhà hàng nhận đặt bàn online. Quy trình "Xác nhận đặt bàn" gồm sub-process "Kiểm tra và giữ bàn" với các bước: Kiểm tra khung giờ trống → Giữ bàn tạm thời → Gửi yêu cầu đặt cọc → Xác nhận thanh toán cọc.
Yêu cầu:
- Liệt kê tối thiểu 2 lỗi nghiệp vụ có thể xảy ra và đặt errorCode cho từng lỗi (ví dụ: hết bàn trong khung giờ, khách không thanh toán cọc trong 10 phút — lưu ý cái thứ hai có thể là timer, hãy phân tích xem nó là lỗi nghiệp vụ hay không).
- Với lỗi "hết bàn", đặt một End Error Event trong sub-process và một Error Boundary Event ở process cha để bắt, dẫn tới luồng "Đề xuất khung giờ thay thế".
- Với lỗi "khách không đủ điều kiện đặt cọc" (ví dụ thẻ bị từ chối), giả định nó được ném từ một Service Task. Vẽ luồng dọn dẹp "Nhả bàn đã giữ tạm" sau khi bắt lỗi.
- Tự đánh giá: có lỗi nào trong số bạn liệt kê thực ra là lỗi kỹ thuật không nên dùng BPMN Error không? Giải thích.
Tóm tắt
Error Handling trong BPMN xoay quanh cặp đôi Throw Error và Catch Error, cho phép bản đồ quy trình của bạn phản ánh trung thực cả những tình huống bất thường chứ không chỉ luồng hạnh phúc.
Những điểm cốt lõi cần khắc ghi:
- Throw Error phát ra lỗi, điển hình qua End Error Event trong sub-process hoặc qua Service Task ném BPMN Error bằng code, mỗi lỗi mang một errorCode.
- Catch Error bắt lỗi qua Error Boundary Event (gắn trên biên activity/sub-process, dùng cho lỗi cục bộ) hoặc Error Start Event trong Event Sub-process (gom xử lý lỗi đến từ nhiều nơi).
- Error trong BPMN luôn là interrupting — không có biến thể non-interrupting.
- Lỗi lan truyền từ trong ra ngoài cho tới khi gặp catcher khớp errorCode; nếu không ai bắt, quy trình thất bại.
- Phân biệt rạch ròi lỗi nghiệp vụ (dùng BPMN Error) với lỗi kỹ thuật (dùng retry/timer/escalation) — đây là ranh giới quan trọng nhất.
- Bắt lỗi xong phải dọn dẹp hệ quả và dẫn tới một End Event rõ ràng, đừng để luồng error treo lơ lửng.