Mở đầu — vì sao bài này quan trọng
Có một sự thật phũ phàng mà nhiều designer mới vào nghề hay quên: phần lớn thời gian, sản phẩm của bạn không chạy trên "happy path" — con đường lý tưởng nơi mọi thứ diễn ra đúng như kế hoạch. Người dùng gõ sai mật khẩu. Mạng 4G ở quán cà phê rớt giữa lúc thanh toán. Server quá tải vào giờ cao điểm. Người dùng bấm vào một link cũ đã bị xóa. Mỗi tình huống đó là một khoảnh khắc mà cảm xúc của người dùng dễ chuyển từ trung tính sang bực bội nhất.
Và đây mới là điểm mấu chốt: lỗi không phải là dấu chấm hết, mà là một khúc cua. Một error state được thiết kế tốt sẽ biến khoảnh khắc thất vọng thành khoảnh khắc người dùng cảm thấy "à, ứng dụng này hiểu mình, nó giúp mình thoát ra". Ngược lại, một thông báo lỗi vô hồn kiểu "Error 500" hay "Đã có lỗi xảy ra" sẽ đẩy người dùng ra khỏi sản phẩm — đôi khi vĩnh viễn.
Trong bài này, chúng ta tập trung riêng vào error states và recovery design — tức là cách thiết kế những màn hình, thông báo và luồng xử lý khi mọi thứ đi chệch hướng, sao cho người dùng luôn có đường thoát. Đây không phải là chuyện viết microcopy (bài 25 đã lo phần đó), cũng không phải về empty state (bài 47) hay loading state (bài 49). Trọng tâm của chúng ta là: phân loại lỗi, thiết kế phản hồi tương ứng, và quan trọng nhất — đưa người dùng trở lại đúng đường (recovery).
Khái niệm cốt lõi
Error state là gì và khác gì với recovery
Error state là trạng thái giao diện hiển thị khi một thao tác thất bại hoặc hệ thống rơi vào tình huống bất thường. Nó trả lời câu hỏi: "Chuyện gì vừa xảy ra?"
Recovery (khôi phục) là phần quan trọng hơn nhưng hay bị bỏ quên: nó trả lời câu hỏi "Giờ tôi phải làm gì để thoát khỏi tình huống này?". Một error state mà không kèm recovery thì giống như một biển báo "Đường cụt" mà không chỉ cho bạn lối quay đầu.
Quy tắc vàng: mọi error state đều phải có ít nhất một hành động khôi phục rõ ràng. Dù chỉ là một nút "Thử lại", một link "Quay về trang chủ", hay một gợi ý cụ thể về điều cần sửa.
Phân loại các loại lỗi
Để thiết kế đúng, trước hết phải gọi tên đúng loại lỗi. Mỗi loại có nguyên nhân, mức độ nghiêm trọng và cách khôi phục khác nhau.
1. Validation error (lỗi nhập liệu). Đây là loại nhẹ nhất và phổ biến nhất — người dùng nhập sai định dạng, bỏ trống trường bắt buộc, mật khẩu chưa đủ mạnh. Lỗi này hoàn toàn nằm trong tầm tay người dùng để sửa. Nguyên tắc: hiển thị lỗi ngay tại trường bị sai (inline), nói rõ sai gì và cách sửa, đừng bao giờ chỉ báo "Có lỗi" rồi để người dùng tự mò.
2. Network error (lỗi mạng). Mất kết nối, timeout, mạng chập chờn. Đặc điểm: lỗi này thường tạm thời và không phải lỗi của người dùng. Recovery điển hình là "Thử lại" kèm khả năng tự động retry. Với bối cảnh Việt Nam — nơi người dùng di chuyển nhiều, dùng 3G/4G trong thang máy, hầm gửi xe, vùng phủ sóng yếu — đây là loại lỗi cực kỳ thường gặp và cần được ưu tiên.
3. Server error (lỗi máy chủ — 500). Hệ thống phía sau gặp sự cố. Người dùng hoàn toàn bất lực, không thể tự sửa. Recovery: trấn an rằng "đây không phải lỗi của bạn", cung cấp nút thử lại, và nếu có thể, một kênh liên hệ hỗ trợ. Tuyệt đối không đổ lỗi hay hiển thị stack trace kỹ thuật.
4. Not found error (404). Trang/tài nguyên không tồn tại — link cũ, sản phẩm đã gỡ, URL gõ sai. Recovery: đưa người dùng về điểm an toàn (trang chủ, ô tìm kiếm, danh mục liên quan). Đây là cơ hội tốt để thể hiện cá tính thương hiệu thay vì để người dùng đi vào ngõ cụt.
5. Permission error (lỗi quyền truy cập — 401/403). Người dùng chưa đăng nhập, phiên hết hạn, hoặc không có quyền xem nội dung. Recovery khác nhau tùy nguyên nhân: nếu chưa đăng nhập → đưa tới màn hình login; nếu hết phiên → cho đăng nhập lại mà không mất dữ liệu đang nhập dở; nếu thiếu quyền → giải thích và chỉ cách xin quyền.
Ba thành phần của một error state tốt
Dù là loại lỗi nào, một error state hoàn chỉnh luôn có ba lớp:
- Nhận diện (Acknowledge) — báo cho người dùng biết có chuyện xảy ra, bằng ngôn ngữ con người chứ không phải mã lỗi.
- Giải thích (Explain) — nói ngắn gọn nguyên nhân ở mức người dùng hiểu được, và quan trọng là ai chịu trách nhiệm (lỗi của bạn cần sửa, hay lỗi hệ thống).
- Hành động (Act) — cho người dùng ít nhất một con đường thoát rõ ràng.
Phân biệt mức độ nghiêm trọng
Không phải lỗi nào cũng cần làm ầm lên. Hãy chia theo mức độ:
- Lỗi chặn (blocking) — người dùng không thể tiếp tục (thanh toán thất bại, mất kết nối khi đang lưu). Cần xử lý nổi bật, thường là modal hoặc full-screen.
- Lỗi không chặn (non-blocking) — chỉ một phần bị ảnh hưởng (một widget trên dashboard load lỗi). Dùng inline error hoặc toast, để phần còn lại của ứng dụng vẫn dùng được.
Tình huống thực tế
Ví dụ 1 — Thanh toán thất bại trên một sàn TMĐT Việt Nam
Hãy hình dung một sàn thương mại điện tử kiểu Tiki hoặc Shopee. Chị Lan đặt một đơn 1.250.000đ, đến bước thanh toán qua thẻ thì giao dịch bị từ chối. Phiên bản tệ: hệ thống hiện một dòng đỏ "Giao dịch thất bại (mã 0x4F2)" và đẩy chị về trang giỏ hàng trống. Chị Lan không biết tiền có bị trừ không, không biết sai ở đâu, và toàn bộ thông tin giao hàng đã nhập phải gõ lại từ đầu.
Phiên bản tốt mà đội thiết kế xây lại: thông báo viết "Thanh toán chưa thành công. Thẻ của bạn chưa bị trừ tiền." — câu thứ hai giải tỏa nỗi lo lớn nhất về tiền bạc. Bên dưới là gợi ý cụ thể theo từng nguyên nhân ("Có thể thẻ chưa đủ số dư, hoặc ngân hàng từ chối giao dịch online — hãy thử lại hoặc chọn phương thức khác"). Hai nút rõ ràng: "Thử lại" và "Đổi phương thức thanh toán". Quan trọng nhất: toàn bộ thông tin giỏ hàng và địa chỉ giao hàng được giữ nguyên.
Bài học rút ra: Trong những luồng liên quan đến tiền, ưu tiên số một của recovery là giải tỏa lo lắng (tiền có mất không?) và bảo toàn dữ liệu người dùng đã nhập. Một con số ước tính từ các sàn TMĐT cho thấy: giữ lại dữ liệu giỏ hàng sau lỗi thanh toán có thể cứu được 20-30% đơn hàng lẽ ra bị bỏ rơi.
Ví dụ 2 — Ứng dụng đặt xe mất mạng giữa chuyến
Một ứng dụng gọi xe kiểu Grab hay Be hoạt động trong môi trường mạng cực kỳ bất ổn — người dùng ở trong xe, đi qua hầm, vùng sóng yếu. Giả sử anh Tuấn đang đặt xe thì mạng rớt đúng lúc bấm "Đặt xe".
Cách thiết kế kém: hiện ngay một popup đỏ "Lỗi kết nối" và hủy yêu cầu, buộc anh Tuấn nhập lại điểm đến. Cách thiết kế tốt: ứng dụng tự động retry âm thầm 2-3 lần trong vài giây (vì mạng có thể chỉ chập chờn tạm thời), hiển thị một banner mềm phía trên "Đang kết nối lại..." thay vì popup gắt. Chỉ khi retry thất bại hoàn toàn, mới hiện thông báo rõ ràng "Mất kết nối mạng. Kiểm tra Wi-Fi hoặc dữ liệu di động rồi thử lại" kèm nút "Thử lại" — và yêu cầu đặt xe của anh Tuấn được lưu lại, không bắt nhập lại.
Bài học rút ra: Với network error, hãy phân biệt "chập chờn tạm thời" và "mất hẳn". Tự động retry với cơ chế exponential backoff (mỗi lần thử cách nhau lâu hơn) xử lý phần lớn trường hợp tạm thời mà người dùng thậm chí không cần biết. Chỉ làm phiền họ khi thực sự cần hành động của họ.
Ví dụ 3 — Phiên đăng nhập hết hạn trong app ngân hàng
Một ứng dụng ngân hàng số (như Timo hay app của các ngân hàng VN) thường tự đăng xuất sau một thời gian không hoạt động vì lý do bảo mật. Chị Hà đang điền form chuyển khoản 5 triệu thì phiên hết hạn (permission/401 error).
Thiết kế tệ: hệ thống đá thẳng ra màn hình login, mọi thông tin chuyển khoản đã nhập bay sạch. Chị Hà ức chế phải làm lại từ đầu. Thiết kế tốt: hiện một modal "Phiên đăng nhập đã hết hạn vì lý do bảo mật. Vui lòng xác thực lại để tiếp tục." với ô nhập lại mật khẩu/sinh trắc học ngay trên modal đó. Sau khi xác thực, chị Hà quay lại đúng form chuyển khoản với mọi thông tin còn nguyên.
Bài học rút ra: Permission error do hết phiên là loại lỗi mà người dùng không làm gì sai — họ chỉ dừng lại hơi lâu. Đừng phạt họ bằng cách xóa dữ liệu. Cân bằng giữa yêu cầu bảo mật (bắt xác thực lại) và sự tôn trọng công sức người dùng (giữ lại context).
Hướng dẫn từng bước
Khi thiết kế error state cho một luồng cụ thể, hãy đi theo quy trình sau:
Bước 1 — Liệt kê các điểm có thể hỏng (failure points). Với mỗi luồng quan trọng (đăng ký, thanh toán, upload...), hãy đi qua từng bước và hỏi "ở đây cái gì có thể sai?". Lập một bảng: bước → loại lỗi (validation/network/server/404/permission) → mức độ (blocking/non-blocking).
Bước 2 — Quyết định nơi hiển thị lỗi. Lỗi nhập liệu → inline ngay tại trường. Lỗi một phần → toast/banner. Lỗi chặn toàn luồng → modal hoặc full-screen. Quy tắc: hiển thị lỗi càng gần điểm phát sinh và càng gần thời điểm xảy ra càng tốt.
Bước 3 — Viết nội dung theo công thức Acknowledge → Explain → Act. Với mỗi lỗi, viết một dòng nhận diện, một dòng giải thích ở ngôn ngữ người dùng, và xác định (các) hành động khôi phục.
Bước 4 — Thiết kế recovery rõ ràng. Mỗi error phải có ít nhất một CTA dẫn người dùng tiến lên hoặc lùi về điểm an toàn. Ưu tiên giữ lại dữ liệu họ đã nhập. Cân nhắc auto-retry cho lỗi mạng.
Bước 5 — Phân tầng phòng thủ. Tự động hóa những gì có thể (retry ngầm), chỉ leo thang lên thông báo rõ ràng khi cần người dùng can thiệp.
Bước 6 — Thêm "lưới an toàn" cuối cùng. Luôn có một error state tổng quát (generic fallback) cho những lỗi không lường trước, kèm nút "Thử lại" và "Về trang chủ" — để không bao giờ có màn hình trắng chết cứng.
Bước 7 — Kiểm tra trên cả ba môi trường. Thử với mạng yếu (dùng network throttling trong DevTools), server giả lập lỗi 500, và các đầu vào sai. Đừng chỉ test happy path.
Lỗi thường gặp & mẹo
Lỗi 1 — Dùng mã lỗi và thuật ngữ kỹ thuật. "Error 500", "Null pointer exception", "Request failed with code 0x...". Người dùng không phải lập trình viên. Mẹo: để lại mã lỗi nhỏ ở góc dưới cho team support tra cứu, nhưng phần chính phải bằng ngôn ngữ con người.
Lỗi 2 — Đổ lỗi cho người dùng. "Bạn đã nhập sai", "Bạn không được phép". Giọng điệu trách móc làm người dùng phòng thủ. Mẹo: dùng giọng trung tính hoặc đứng về phía họ — "Mật khẩu chưa khớp, thử lại nhé" thay vì "Bạn nhập sai mật khẩu".
Lỗi 3 — Error state không có lối thoát. Màn hình báo lỗi nhưng không có nút nào để đi tiếp. Mẹo: mọi error state phải có tối thiểu một CTA. Đây là quy tắc không thể thỏa hiệp.
Lỗi 4 — Xóa sạch dữ liệu người dùng đã nhập khi lỗi. Đặc biệt tệ với form dài. Mẹo: luôn bảo toàn input. Sau khi sửa lỗi, người dùng phải tiếp tục từ chỗ dở dang, không phải làm lại.
Lỗi 5 — Dùng modal full-screen cho mọi lỗi nhỏ. Làm gián đoạn không cần thiết. Mẹo: chọn mức độ hiển thị tương xứng với mức độ nghiêm trọng.
Lỗi 6 — Quên trạng thái offline hoàn toàn. Nhiều app chỉ test khi có mạng. Mẹo: thiết kế riêng cho trường hợp offline — cho phép xem nội dung đã cache, xếp hàng các thao tác để gửi lại khi có mạng.
Lỗi 7 — Báo lỗi quá nhanh khi mạng chỉ chập chờn. Mẹo: thêm độ trễ hợp lý và auto-retry trước khi kết luận là lỗi thật.
Mẹo vàng: Hãy dùng đúng tông giọng theo ngữ cảnh. Lỗi nhẹ (validation) có thể nhẹ nhàng, thậm chí hài hước. Nhưng lỗi nghiêm trọng (mất tiền, mất dữ liệu) thì phải nghiêm túc, rõ ràng, trấn an — đùa cợt lúc người dùng mất tiền là tự sát.
Bài tập thực hành
Bài tập 1 — Lập bản đồ lỗi (Error map). Chọn một luồng quan trọng trong một app bạn dùng hàng ngày (ví dụ: đặt món trên một app giao đồ ăn). Liệt kê tối thiểu 6 điểm có thể hỏng, phân loại từng điểm theo 5 loại lỗi đã học, và đánh dấu blocking/non-blocking.
Bài tập 2 — Thiết kế lại 3 error state. Với 3 trong số các lỗi vừa liệt kê, thiết kế (vẽ trên Figma hoặc giấy) error state hoàn chỉnh theo công thức Acknowledge → Explain → Act. Mỗi màn hình phải có ít nhất một hành động khôi phục và phương án bảo toàn dữ liệu nếu có form.
Bài tập 3 — Viết lại microcopy "xấu". Lấy 5 thông báo lỗi tệ ngoài đời thực ("Đã có lỗi xảy ra", "Error 404", "Yêu cầu thất bại"...) và viết lại mỗi cái thành phiên bản nhân văn, có lối thoát rõ ràng.
Bài tập 4 — Thiết kế luồng recovery cho mất mạng. Vẽ luồng đầy đủ cho tình huống người dùng mất mạng khi đang gửi một form quan trọng: từ lúc auto-retry ngầm, đến banner "đang kết nối lại", đến thông báo lỗi cuối cùng nếu thất bại — kèm cách giữ lại dữ liệu.
Tóm tắt
Error state và recovery là phần "không hào nhoáng" nhưng quyết định lớn đến mức độ tin cậy của sản phẩm. Những điểm cốt lõi cần nhớ:
- Lỗi là một khúc cua, không phải dấu chấm hết. Mục tiêu của bạn luôn là đưa người dùng trở lại đường đi, không phải để họ mắc kẹt.
- Năm loại lỗi cần phân biệt: validation (người dùng sửa được), network (thường tạm thời, không phải lỗi của họ), server/500 (lỗi hệ thống, cần trấn an), 404 (đưa về điểm an toàn), permission (xử lý theo nguyên nhân, giữ context).
- Công thức Acknowledge → Explain → Act — và lớp "Act" là không thể thiếu: mọi error state phải có ít nhất một hành động khôi phục.
- Bảo toàn dữ liệu người dùng là một trong những hành động tử tế và hiệu quả nhất, đặc biệt trong luồng thanh toán và form dài.
- Tương xứng mức độ: chọn cách hiển thị (inline, toast, modal, full-screen) theo độ nghiêm trọng; tự động hóa retry cho lỗi mạng và chỉ làm phiền người dùng khi thực sự cần.
- Giọng văn nhân văn, không thuật ngữ kỹ thuật, không đổ lỗi, và luôn có một generic fallback làm lưới an toàn cuối cùng.