การจัดการ Error
Error as value — philosophy ของ Go
หัวข้อที่มีชื่อว่า “Error as value — philosophy ของ Go”TypeScript ใช้ throw กับ try/catch ส่วน Go ถือว่า error เป็น return value ธรรมดา: ทุกฟังก์ชันที่อาจ fail จะคืน (result, error) คุณต้องเช็ค error อย่างชัดเจน — compiler ไม่ยอมให้เพิกเฉยแบบเงียบ ๆ Echo ต่อยอดแนวคิดนี้แบบธรรมชาติ: handler ที่คืน error ที่ไม่เป็น nil จะ trigger error handler ของ Echo
// TypeScript / Express — throw-basedasync function getBook(id: string): Promise<Book> { const book = await db.findOne(id); if (!book) throw new NotFoundException('book not found'); return book;}
// Express error middleware at the bottomapp.use((err, req, res, next) => { const status = err.status ?? 500; res.status(status).json({ message: err.message });});// Go / Echo — return-basedfunc findBook(ctx context.Context, id string) (Book, error) { book, err := store.FindByID(ctx, id) if err != nil { return Book{}, fmt.Errorf("findBook: %w", err) } return book, nil}
// Handler — propagate the error to Echo's error handlerfunc getBook(c *echo.Context) error { book, err := findBook(c.Request().Context(), c.Param("id")) if err != nil { return echo.NewHTTPError(http.StatusNotFound, err.Error()) } return c.JSON(http.StatusOK, book)}echo.NewHTTPError — error ที่รู้จัก HTTP
หัวข้อที่มีชื่อว่า “echo.NewHTTPError — error ที่รู้จัก HTTP”echo.NewHTTPError(statusCode, message) สร้าง *echo.HTTPError (ใน v5 message เป็น string ธรรมดา) เมื่อ handler คืนค่านี้ default error handler ของ Echo จะ serialize เป็น {"message": "..."} พร้อม HTTP status code ที่ถูกต้อง สำหรับ error ธรรมดา Echo จะคืน 500 Internal Server Error
error ตัวไหนก็ตามที่ implement StatusCode() int จะได้รับการปฏิบัติแบบเดียวกัน — error สำเร็จรูป (echo.ErrNotFound, echo.ErrUnauthorized, …) ทำงานด้วยวิธีนี้ ถ้าอยากอ่าน status กลับออกมาจาก error ใด ๆ ใช้ echo.StatusCode(err) ซึ่งจะคืน 0 เมื่อ error นั้นไม่มี status
// 400return echo.NewHTTPError(http.StatusBadRequest, "invalid id format")
// 401return echo.NewHTTPError(http.StatusUnauthorized, "missing token")
// 404return echo.NewHTTPError(http.StatusNotFound, "book not found")
// 409return echo.NewHTTPError(http.StatusConflict, "book already exists")Wrapping error พร้อม context
หัวข้อที่มีชื่อว่า “Wrapping error พร้อม context”ห่อ error ด้วย fmt.Errorf("context: %w", err) เสมอ เพื่อให้ error ต้นฉบับถูกเก็บไว้สำหรับการตรวจสอบด้วย errors.Is / errors.As และสำหรับ structured logging
// TypeScript — wrapping with causethrow new Error('failed to save book', { cause: originalError });
// NestJS — HTTP-specificthrow new InternalServerErrorException('failed to save book');// Go — wrapping with %wfunc (s *BookService) Create(ctx context.Context, req CreateBookRequest) (Book, error) { book, err := s.store.Insert(ctx, req) if err != nil { // %w preserves the original error for errors.Is / errors.As return Book{}, fmt.Errorf("BookService.Create: %w", err) } return book, nil}
// In the handler — check error typefunc createBook(c *echo.Context) error { book, err := svc.Create(c.Request().Context(), req) if err != nil { // errors.Is walks the wrap chain if errors.Is(err, store.ErrDuplicate) { return echo.NewHTTPError(http.StatusConflict, "book already exists") } return echo.NewHTTPError(http.StatusInternalServerError, "failed to create book") } return c.JSON(http.StatusCreated, book)}Central error handler
หัวข้อที่มีชื่อว่า “Central error handler”Echo ให้คุณแทน default error handler ด้วย e.HTTPErrorHandler ตรงนี้คือจุดที่คุณจะ log error, แปลง domain error เป็น HTTP code และทำให้ response envelope สม่ำเสมอ ตัว default เป็น factory — echo.DefaultHTTPErrorHandler(exposeError bool) — และมัน ไม่ log ให้ การ log เป็นหน้าที่ของคุณ (หรือของ middleware)
// Express — error middleware (4-arg function, must be last)app.use((err: Error, req: Request, res: Response, next: NextFunction) => { console.error(err); if (err instanceof HttpException) { return res.status(err.status).json({ message: err.message }); } res.status(500).json({ message: 'internal server error' });});// Echo — custom HTTP error handler (v5: context first, error second)func customErrorHandler(c *echo.Context, err error) { // Never write twice: a middleware may already have sent a response. if resp, uerr := echo.UnwrapResponse(c.Response()); uerr == nil && resp.Committed { return }
// Anything carrying an HTTP status (echo.NewHTTPError, echo.ErrNotFound, ...) if code := echo.StatusCode(err); code != 0 { var he *echo.HTTPError msg := http.StatusText(code) if errors.As(err, &he) { msg = he.Message } _ = c.JSON(code, map[string]any{"message": msg}) return }
// Unknown error — log it (slog), return 500 c.Logger().Error("unhandled error", "err", err) _ = c.JSON(http.StatusInternalServerError, map[string]any{ "message": "internal server error", })}
// Register in main():e.HTTPErrorHandler = customErrorHandlerสองจุดของ v5 ที่ควรจำ: c.Response() ตอนนี้เป็น http.ResponseWriter ธรรมดา ข้อมูลภายในของ Echo (Status, Size, Committed) จึงต้องดึงผ่าน echo.UnwrapResponse และ errors.As(err, &he) จับได้เฉพาะ error ที่สร้างจาก echo.NewHTTPError เท่านั้น — ค่า echo.Err* สำเร็จรูปเป็นคนละ type (ไม่ export) นี่คือเหตุผลที่ handler ถาม echo.StatusCode ก่อน
รันบนเครื่องของคุณ — ต้องการ Echo module และ network port