Skip to content

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

go
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 ตามได้:

go
row, dbErr := db.Query(...)
if dbErr != nil {
    return core.Wrap(dbErr, "loading user")     // 500 + cause = dbErr
}

ถ้า error ที่ห่ออยู่แล้วเป็น *core.Error (เช่น sentinel) → คง status/code/fields เดิม แค่เติมข้อความ context เข้าไป:

go
return core.Wrap(errmsgs.NotFound, "loading user")  // ยังเป็น 404 NOT_FOUND

Sentinel ที่ใช้ซ้ำได้ (package errmsgs)

go
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 ได้แม้ถูกห่อหลายชั้น:

go
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 ร่วมกัน:

go
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:

go
func doWork() (err error) {
    defer core.Recover(&err)
    // ... code ที่อาจ panic ...
    return nil
}

ดู stack trace

go
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 บนรายงานซ้ำ:

go
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:

go
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 ระหว่างทางห่อไว้:

jsonc
// 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

go
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)

Maintained by Passakon Puttasuwan & Dev Core Team.