การจัดการ Error
Express error middleware vs Axum Result
หัวข้อที่มีชื่อว่า “Express error middleware vs Axum Result”ใน Express errors propagate ผ่าน next(err) ไปยัง 4-argument error handler ที่ลงทะเบียนที่ด้านล่างของ middleware stack ใน Axum ไม่มี global error handler แต่ handlers return Result<T, E> ที่ E implement IntoResponse แทน ถ้า handler return Err(e) Axum จะเรียก e.into_response() โดยอัตโนมัติ
// Express — propagate error to error handlerapp.get('/users/:id', async (req, res, next) => { try { const user = await db.findUser(req.params.id); if (!user) return next(new NotFoundError('User not found')); res.json(user); } catch (err) { next(err); // pass to error middleware }});
// 4-argument error handlerapp.use((err, req, res, next) => { const status = err.status ?? 500; res.status(status).json({ error: err.message });});use axum::{ http::StatusCode, response::{IntoResponse, Json, Response},};use serde_json::json;
// Custom error type#[derive(thiserror::Error, Debug)]enum AppError { #[error("not found: {0}")] NotFound(String), #[error("database error")] Database(#[from] sqlx::Error),}
// Implement IntoResponse so Axum can convert errors to HTTP responsesimpl IntoResponse for AppError { fn into_response(self) -> Response { let (status, message) = match &self { AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()), AppError::Database(_) => ( StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".to_string(), ), }; (status, Json(json!({ "error": message }))).into_response() }}
// Handler returns Result<Json<User>, AppError>async fn get_user(Path(id): Path<u32>) -> Result<Json<User>, AppError> { let user = db_find(id).await.map_err(AppError::Database)?; user.ok_or_else(|| AppError::NotFound(format!("user {} not found", id))) .map(Json)}Operator ?
หัวข้อที่มีชื่อว่า “Operator ?”? operator คือ shorthand สำหรับ early-return ของ Result ใน Rust ใช้แทน try/catch สำหรับ error ที่กู้คืนได้:
async fn get_user(Path(id): Path<u32>) -> Result<Json<User>, AppError> { // ? returns Err(AppError::Database(...)) early if the query fails let user = db_find(id).await?;
// Option::ok_or converts None to Err let user = user.ok_or_else(|| AppError::NotFound(format!("user {id} not found")))?;
Ok(Json(user))}ใช้ thiserror สำหรับ error types ที่สะอาด
หัวข้อที่มีชื่อว่า “ใช้ thiserror สำหรับ error types ที่สะอาด”thiserror generates Display และ Error implementations จาก attribute macros — เทียบเท่ากับการกำหนด discriminated union ของ error cases ใน TypeScript:
// TypeScript — discriminated uniontype AppError = | { kind: 'NotFound'; message: string } | { kind: 'Validation'; fields: string[] } | { kind: 'Database'; cause: Error };
function toHttpError(err: AppError): [number, string] { switch (err.kind) { case 'NotFound': return [404, err.message]; case 'Validation': return [422, err.fields.join(', ')]; case 'Database': return [500, 'Internal server error']; }}use thiserror::Error;
#[derive(Error, Debug)]pub enum AppError { #[error("not found: {0}")] NotFound(String),
#[error("validation error: {0}")] Validation(String),
#[error("database error: {0}")] Database(#[from] sqlx::Error),}
impl IntoResponse for AppError { fn into_response(self) -> Response { match self { AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg).into_response(), AppError::Validation(msg) => (StatusCode::UNPROCESSABLE_ENTITY, msg).into_response(), AppError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), } }}รันบนเครื่องของคุณ — การจัดการ error ต้องการ Axum server ที่กำลังทำงาน ทดสอบด้วย
curl -v http://localhost:3000/users/999เพื่อดู error response