Giao diện
Xử lý lỗi
Mọi lỗi trong app — lỗi HTTP, lỗi mạng, lỗi JavaScript — đều quy về một kiểu duy nhất: AppError. Nhờ vậy chỗ nào cũng xử lý được theo cùng một cách, và giao diện báo lỗi nhất quán giữa các màn hình.
ts
class AppError extends Error {
readonly status?: number // mã HTTP — KHÔNG phải statusCode
readonly code?: string // mã nghiệp vụ, nếu backend có trả
readonly data?: unknown // body của response lỗi
}Field là status, không phải statusCode
Đây là khác biệt với createError của h3 (dùng statusCode). Viết error.statusCode ở phía client sẽ luôn nhận undefined, và mọi nhánh if dựa vào nó im lặng chạy sai.
toAppError
useApi đã chuyển lỗi sang AppError sẵn. Với lỗi từ nguồn khác ($fetch trực tiếp, thư viện, lỗi tự ném), gọi toAppError để về cùng một dạng:
ts
import { toAppError } from '@tasco/utils'
try {
await doSomething()
} catch (caught) {
const error = toAppError(caught)
error.status // 422
error.message // thông báo đọc được cho người dùng
error.data // body gốc, ví dụ { message, errors }
}| Đầu vào | Kết quả |
|---|---|
AppError | Trả lại chính nó |
| Response lỗi của ofetch | status từ response, data từ body, message lấy data.message hoặc data.error, không có thì statusText |
Error thường | message giữ nguyên, không có status |
| Chuỗi | Thành message |
| Bất kỳ thứ gì khác | Đã xảy ra lỗi không xác định, giá trị gốc nằm ở cause |
Phân loại
ts
import {
isAuthError, isForbidden, isValidation, isServerError, isNetworkError, errorSeverity,
} from '@tasco/utils'| Hàm | Đúng khi | Framework phản ứng |
|---|---|---|
isAuthError | status === 401 | Điều hướng về trang đăng nhập |
isForbidden | status === 403 | Hiện thông báo |
isValidation | status === 422 | Không hiện toast — form tự hiện lỗi từng field |
isServerError | status >= 500 | Hiện notification "Lỗi hệ thống" |
isNetworkError | không có status | Hiện notification |
errorSeverity(error) trả 'error' hoặc 'warning' — dùng khi tự dựng giao diện báo lỗi.
Quy ước trong composable
Bật loading, gán error, luôn tắt loading trong finally:
ts
export function useOrderDetail(id: string) {
const { fetchOrder } = useOrdersApi()
const order = ref<Order | null>(null)
const loading = ref(false)
const error = ref<AppError | null>(null)
async function load() {
loading.value = true
error.value = null
try {
order.value = await fetchOrder(id)
} catch (caught) {
error.value = toAppError(caught)
} finally {
loading.value = false
}
}
return { order, loading, error, load }
}Hàm phân tích và hàm kiểm tra
Không phải lỗi nào cũng nên ném ra ngoài:
ts
/** parse* trả null khi dữ liệu không hợp lệ — người gọi tự quyết định. */
function parseOrderCode(input: string): string | null {
return /^DH-\d{4}$/.test(input) ? input : null
}
/** validate* trả kết quả có cấu trúc, kèm lý do để hiện lên giao diện. */
function validateAmount(value: number): { ok: true, payload: number } | { ok: false, reason: string } {
if (value <= 0) return { ok: false, reason: 'Giá trị phải lớn hơn 0' }
return { ok: true, payload: value }
}Ném lỗi dành cho tình huống bất thường (mạng hỏng, backend từ chối), không dành cho luồng người dùng nhập sai.
Đừng để promise bị bỏ rơi
ts
// Sai — lỗi biến mất, console hiện "unhandled rejection"
onMounted(() => { void loadData() })
// Đúng — hoặc await, hoặc bắt tường minh
onMounted(async () => {
try {
await loadData()
} catch (caught) {
$tascoError(caught)
}
})Trong template, bind thẳng hàm trả Promise vào sự kiện là an toàn: lỗi đi qua handler chung của Vue và tới error handler của framework.
Lỗi do dùng sai API
Khi tự viết composable dùng chung cho nhiều màn hình, lỗi lập trình nên nói rõ cách sửa:
ts
throw new Error('[@tasco/orders] useOrderDetail cần id khác rỗng — truyền route.params.id.')Liên quan
- Hiển thị lỗi — toast, notification, trang lỗi.
- Lỗi 422 và form — gắn lỗi vào đúng ô nhập.
- Hợp đồng dữ liệu — hình dạng body lỗi mà framework trông đợi.