ข้ามไปยังเนื้อหา

การ Wrap Error

เมื่อฟังก์ชันเรียกใช้อีกฟังก์ชันแล้วล้มเหลว การคืน error เดิมทำให้เสียบริบท ผู้เรียกเห็นแค่ “database error” แต่ไม่รู้ว่า operation ใดที่ทำให้เกิด request ใดที่เกี่ยวข้อง หรือเกิดที่ layer ไหน การ wrap เพิ่มชั้น context ว่ากำลังทำอะไรอยู่ โดยยังเก็บ error เดิมไว้ให้ตรวจสอบได้

รูปแบบ idiomatic ใน Go:

if err != nil {
return fmt.Errorf("description: %w", err)
}

%w คือกุญแจสำคัญ เพราะ wrap error เดิมไว้ภายใน error ใหม่ รักษา chain ทั้งหมดไว้สำหรับ caller ที่ต้องการตรวจสอบ

fmt.Errorf สร้าง error ใหม่ที่มี message รวมค่าอื่น แต่วิธีรวม error argument ขึ้นอยู่กับ verb ที่เลือก:

  • %v จัดรูปแบบ message string ของ error และฝังเป็น plain text type ของ error เดิมหายไป chain ถูกตัด
  • %w บันทึก error เดิมไว้ใน error ใหม่ message ยังมี message เดิม แต่ error เดิมยังสามารถเข้าถึงได้โดยใช้โปรแกรม
base := errors.New("connection refused")
// %v — message only, chain is severed
e1 := fmt.Errorf("dial: %v", base)
fmt.Println(errors.Is(e1, base)) // false
// %w — wraps, chain is preserved
e2 := fmt.Errorf("dial: %w", base)
fmt.Println(errors.Is(e2, base)) // true

ใช้ %w เมื่อต้องการให้ caller upstream สามารถระบุหรือดึง error เดิมออกมาได้ ใช้ %v เฉพาะเมื่อตั้งใจจะทิ้ง error type เดิม เช่น เมื่อ log และ chain ไม่มีความสำคัญ

errors.Is(err, target) เดิน unwrap chain ทั้งหมดของ err และคืนค่า true ถ้า error ใดใน chain ตรงกับ target การ match ทำโดย identity (==) ตามค่าเริ่มต้น ไม่ใช่ตาม message string

var ErrNotFound = errors.New("not found")
err := fmt.Errorf("getUser: %w", fmt.Errorf("queryDB: %w", ErrNotFound))
fmt.Println(errors.Is(err, ErrNotFound)) // true — found deep in the chain

ทำงานได้ไม่ว่า target จะถูก wrap ลึกแค่ไหน นิยาม sentinel error ครั้งเดียว (เป็น var ระดับ package) และตรวจสอบได้ทุกที่ใน call stack

errors.As(err, &target) เดิน chain เช่นกัน แต่แทนที่จะตรวจสอบ identity จะตรวจสอบ type ถ้า error ใดใน chain สามารถ assign ให้ type ของ target ได้ จะทำการ assign และคืน true

var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println(ve.Field) // access fields on the concrete type
}

ใช้ errors.As เมื่อมี custom error type ที่เก็บข้อมูลเพิ่มเติม เช่น field names, HTTP status codes, retry hints และต้องการเข้าถึง field เหล่านั้น ไม่ใช่แค่ตรวจว่า error มีอยู่

struct ใดก็ตามที่ implement Error() string ตรงตาม error interface custom types ช่วยให้เก็บ structured context ควบคู่กับ message:

type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: field %q %s", e.Field, e.Message)
}

เพื่อให้ custom error type เป็นส่วนหนึ่งของ wrapping chain ให้ implement Unwrap() error:

type AppError struct {
Code int
Err error
}
func (e *AppError) Error() string { return fmt.Sprintf("code %d: %v", e.Code, e.Err) }
func (e *AppError) Unwrap() error { return e.Err }

เมื่อมี Unwrap แล้ว errors.Is และ errors.As สามารถมองผ่าน AppError เพื่อไปถึง error ที่ครอบอยู่ข้างใน

package main
import (
"errors"
"fmt"
)
var ErrDatabase = errors.New("database error")
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: field %q %s", e.Field, e.Message)
}
func queryDB(id int) (string, error) {
if id < 0 {
return "", ErrDatabase
}
if id == 0 {
return "", &ValidationError{Field: "id", Message: "must be positive"}
}
return "Alice", nil
}
func getUser(id int) (string, error) {
name, err := queryDB(id)
if err != nil {
return "", fmt.Errorf("getUser %d: %w", id, err)
}
return name, nil
}
func main() {
_, err := getUser(-1)
if err != nil {
fmt.Println(err)
fmt.Println("is ErrDatabase:", errors.Is(err, ErrDatabase))
}
_, err = getUser(0)
if err != nil {
fmt.Println(err)
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Printf("field=%q msg=%q\n", ve.Field, ve.Message)
}
}
name, err := getUser(1)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("name:", name)
}
สิ่งที่ได้ประโยชน์ต้นทุน
fmt.Errorf(“%w”, err)wrap พร้อม context, errors.Is/As ทำงานได้verbose กว่า return err เฉยๆ
errors.Is() ใน chainตรวจ sentinel ใน chain ลึกแค่ไหนก็ได้ช้ากว่า == comparison (traversal)
custom error typeเก็บ structured context (field, code, status)ต้อง maintain type, implement Error()
%v vs %w ใน fmt.Errorf%v แสดง message ใน string, %w preserve chainสับสนง่ายระหว่างสอง verb
  • wrap error ทุกชั้นยิ่งให้ context ยิ่งดี — wrapping ลึกเกินไปทำ error message อ่านยาก ให้ wrap เฉพาะเมื่อ caller ต้องการ context นั้น
  • errors.Unwrap() คืน nil เสมอถ้าไม่ได้ wrap — errors.Unwrap() เรียกบน non-wrapped error คืน nil — ไม่ panic
  • %w ใน fmt.Errorf รองรับ error เดียวเท่านั้น — Go 1.20+ รองรับ fmt.Errorf("msg: %w and %w", e1, e2) เพื่อ wrap หลาย error
  • errors.Is เปรียบเทียบ message string — errors.Is ตรวจ identity (==) ไม่ใช่ message string — sentinel errors ต้องเป็น package-level var

💡 ตัวอย่างจากของจริง

Kubernetes controller ทุก layer เพิ่ม context ด้วย wrapping — fmt.Errorf("reconcile pod %s: %w", pod.Name, err) — ทำให้ stack ของ log มีความหมาย

Go database drivers wrap sql error เพิ่ม query, table, column context ให้ developer debug ได้ทันที

ความแตกต่างระหว่าง %v และ %w ใน fmt.Errorf คืออะไร?
errors.Is(err, ErrNotFound) คืนค่า true เมื่อ:
errors.As(err, &target) ทำอะไร?
เพื่อให้ custom error type สามารถ unwrap ได้ ต้อง implement: