Error Handling
idin-core v2 มี error แบบ structured ตัวเดียวที่ไหลผ่านทุก layer — core.IError (interface) กับ core.Error (concrete) ชื่อและ method เหมือน v1 แต่ข้างในแก้บั๊ก: ไม่ panic, รองรับ errors.Is/As, และเก็บ stack trace ให้อัตโนมัติ
ทุก module คืน
core.IError— cache, database, mongo, mq, storage, mailer, push, jwt, csv, validation, repository, requester, auth, scheduler ล้วนคืนIError(ไม่ใช่errorเปล่า) จึงreturn errจาก handler ได้ตรงๆ และเรียกerr.GetCode()/err.GetStatus()ได้โดยไม่ต้อง type-assert (ยกเว้น callback ที่ผู้ใช้เขียนเอง เช่นJobFunc,TokenVerifier,Remember's loader ที่ยังรับerrorเพื่อความยืดหยุ่น)
แนวคิด
- ทุก error ที่คืนออกจาก framework/handler เป็น
core.IError - มี
GetCode()(machine-readable),GetStatus()(HTTP status),GetMessage()(ข้อความ) - serialise เป็น JSON ด้วย
JSON()→{ "code": ..., "message": ..., "fields"?: ... }
สร้าง error
import core "gitlab.finema.co/finema/idin-core/v2"
// ระบุ status + code + message เอง
err := core.New(http.StatusBadRequest, "BAD_REQUEST", "bad request")
// แบบมี format
err := core.Newf(http.StatusConflict, "CONFLICT", "user %d already exists", id)ห่อ error จากภายนอก (Wrap)
Wrap เปลี่ยน error ธรรมดาให้เป็น *core.Error (default 500) โดย ไม่ panic และ เก็บ cause ไว้ให้ errors.Is/As ตามได้:
row, dbErr := db.Query(...)
if dbErr != nil {
return core.Wrap(dbErr, "loading user") // 500 + cause = dbErr
}ถ้า error ที่ห่ออยู่แล้วเป็น *core.Error (เช่น sentinel) → คง status/code/fields เดิม แค่เติมข้อความ context เข้าไป:
return core.Wrap(errmsgs.NotFound, "loading user") // ยังเป็น 404 NOT_FOUNDSentinel ที่ใช้ซ้ำได้ (package errmsgs)
import "gitlab.finema.co/finema/idin-core/v2/errmsgs"
return errmsgs.NotFound // 404 NOT_FOUND
return errmsgs.BadRequest // 400 BAD_REQUEST
return errmsgs.Unauthorized // 401 UNAUTHORIZED
return errmsgs.DBError // 500 DATABASE_ERROR
return errmsgs.NotFoundCustomError("user") // 404 USER_NOT_FOUNDเทียบ error ด้วย errors.Is / errors.As
Sentinel เทียบกันด้วย code จึง match ได้แม้ถูกห่อหลายชั้น:
if errors.Is(err, errmsgs.NotFound) {
// จัดการ 404
}
var e *core.Error
if errors.As(err, &e) {
log.Println(e.GetStatus(), e.GetCode())
}ปรับแต่งแบบ chain (ไม่ mutate ตัวเดิม)
builder ทุกตัวคืน copy ใหม่ ปลอดภัยต่อการใช้ sentinel ร่วมกัน:
return errmsgs.BadRequest.
WithCode("INVALID_EMAIL").
WithMessage("email is not valid").
WithFields(map[string]any{"email": "REQUIRED"})แปลง panic เป็น error (Recover)
core.Recover(&err) ใน defer เปลี่ยน panic เป็น *core.Error พร้อม stack — ต่างจาก v1 ที่ re-panic แล้วทิ้ง stack:
func doWork() (err error) {
defer core.Recover(&err)
// ... code ที่อาจ panic ...
return nil
}ดู stack trace
var e *core.Error
if errors.As(err, &e) {
fmt.Println(e.StackString()) // "func\n\tfile:line" ต่อ frame
}ส่งเข้า Sentry อัตโนมัติ
error ที่ status ≥ 500 ถูกส่งเข้า Sentry ตั้งแต่ตอนที่ ctx.NewError(...) สร้างมันขึ้นมา (จุดที่ยังมี context ครบที่สุด) พร้อม user / scoped data / breadcrumb / stack แล้วผูก event id กลับเข้า error เพื่อไม่ให้ layer บนรายงานซ้ำ:
err := c.NewError(dbErr, errmsgs.DBError)
var e *core.Error
errors.As(err, &e)
e.EventID() // event id ใน Sentry ("" ถ้าไม่ได้ตั้ง DSN)error ที่ ไม่ได้ return แต่ log ทิ้งไว้ก็ถูกส่งเหมือนกัน ถ้า status ≥ 500:
ctx.Log().Error("ดึงเรตไม่ได้ ใช้เรตเก่า", "err", err) // ส่งถ้า err เป็น 5xxและ error ตัวเดียวถูกรายงาน ครั้งเดียว เสมอ ไม่ว่าจะ log ก่อนแล้ว return ทีหลัง หรือกลับกัน (event id ผูกอยู่กับตัว error) — รายละเอียดทั้งหมดอยู่ที่ Sentry
ข้อความจริงตอน dev (ENV=dev)
เมื่อ ENV=dev ฟิลด์ message ใน response จะถูกแทนด้วยข้อความของ root cause — ข้อความที่ driver/library รายงานจริง ไม่ใช่ป้ายที่ layer ระหว่างทางห่อไว้:
// ENV=dev
{ "code": "DATABASE_ERROR",
"message": "ERROR: duplicate key value violates unique constraint \"users_email_key\"" }
// ENV อื่น (รวมถึงไม่ได้ตั้ง) — ใช้ message ของ error type เท่านั้น
{ "code": "DATABASE_ERROR", "message": "repository" }- ใช้กับ error ที่ มี cause เท่านั้น — validation error กับ sentinel ที่ไม่มี cause ยังคงข้อความและ
fieldsเดิมทุก environment - panic ก็เข้ากติกาเดียวกัน: dev เห็นข้อความ panic, environment อื่นเห็น
Internal server errorตามเดิม codeไม่เปลี่ยนตาม environment — client จึงยัง match ด้วย code ได้เหมือนกันทุกที่- ไม่ได้ตั้ง
ENV= ไม่ใช่ dev จึงไม่รั่วโดยบังเอิญ (แต่ควรตั้งENVเสมออยู่ดี)
อยากได้ root cause ในโค้ดเองใช้ core.RootCause(err)
JSON response shape
b, _ := json.Marshal(err.(core.IError).JSON())
// { "code": "INVALID_PARAMS", "message": "Invalid parameters", "fields": {...} }Statusไม่ถูก serialise (ใช้ตอน set HTTP status code เท่านั้น)fieldsจะหายไปถ้าไม่มีค่า (omitempty) — ใช้กับ validation error (ดู validation docs)