go-errors
Go Error Handling
Core Principle
Errors are values in Go. Handle them explicitly — don't hide them, don't panic for expected failures.
Error Interface
type error interface {
Error() string
}
Creating Errors
// Simple errors
import "errors"
var ErrNotFound = errors.New("not found")
// Formatted errors
import "fmt"
return fmt.Errorf("user %d not found", id)
// Wrapped errors (Go 1.13+) — preserves cause chain
return fmt.Errorf("fetching user: %w", err)
// Custom error type
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
Error Wrapping & Unwrapping
// Wrap with context
if err := db.Query(sql); err != nil {
return fmt.Errorf("querying users: %w", err)
}
// errors.Is — check for specific error in chain
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
// errors.As — extract specific error type from chain
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("Failed at path:", pathErr.Path)
}
| Verb | Effect |
|---|---|
%w |
Wraps error — errors.Is / errors.As can unwrap |
%v |
Formats error — wrapping chain is lost |
When to Wrap vs Not
| Scenario | Action | Why |
|---|---|---|
| Adding context | fmt.Errorf("...: %w", err) |
Preserves chain for callers |
| Hiding implementation detail | fmt.Errorf("...: %v", err) or new error |
Don't leak internal types |
| Matching sentinel errors | Use %w |
Callers can errors.Is |
| Library boundary | Consider carefully | Wrapping couples caller to your dependencies |
Sentinel Errors
// Package-level error values
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrConflict = errors.New("conflict")
)
// Usage
func FindUser(id int) (*User, error) {
user, err := db.Get(id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("finding user %d: %w", id, err)
}
return user, nil
}
| Sentinel rule | Detail |
|---|---|
Name with Err prefix |
ErrNotFound, ErrTimeout |
Package-level var |
Not const — must be comparable with errors.Is |
| Use sparingly | Only for errors callers need to check programmatically |
| Document them | Part of your API contract |
Custom Error Types
type NotFoundError struct {
Resource string
ID int
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s %d not found", e.Resource, e.ID)
}
// Optional: wrap underlying error
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string {
return fmt.Sprintf("query %q: %v", e.Query, e.Err)
}
func (e *QueryError) Unwrap() error {
return e.Err // enables errors.Is/errors.As
}
Errors as Values
Errors are programmable values — use patterns to reduce repetitive if err != nil:
// ErrorWriter: accumulates error, check once at end
type ErrorWriter struct {
w io.Writer
err error
}
func (ew *ErrorWriter) Write(buf []byte) {
if ew.err != nil { return }
_, ew.err = ew.w.Write(buf)
}
// Usage — chain writes, check once
ew := &ErrorWriter{w: w}
ew.Write(header)
ew.Write(body)
ew.Write(footer)
if ew.err != nil {
return ew.err
}
No In-Band Errors
// BAD: -1 or empty string as error signal
func Lookup(key string) string {
if _, ok := m[key]; !ok { return "" } // "" might be valid!
return m[key]
}
// GOOD: return additional value
func Lookup(key string) (string, bool) {
v, ok := m[key]
return v, ok
}
// GOOD: return error
func Lookup(key string) (string, error) {
v, ok := m[key]
if !ok { return "", ErrNotFound }
return v, nil
}
Handle Every Error
// BAD: silently ignored
f.Close()
// GOOD: handle or explicitly acknowledge
if err := f.Close(); err != nil {
log.Printf("closing file: %v", err)
}
// OK: documented why it's safe to ignore
n, _ := buf.Write(data) // bytes.Buffer.Write never returns error
Handling Patterns
The Standard Pattern
result, err := doSomething()
if err != nil {
return fmt.Errorf("doing something: %w", err)
}
// use result
Multiple Cleanup Actions
func process() (err error) {
f, err := os.Open("file.txt")
if err != nil {
return err
}
defer f.Close()
w, err := os.Create("output.txt")
if err != nil {
return err
}
defer func() {
closeErr := w.Close()
if err == nil {
err = closeErr // capture close error if no prior error
}
}()
_, err = io.Copy(w, f)
return err
}
Error Type Switch
switch err := err.(type) {
case nil:
// success
case *ValidationError:
http.Error(w, err.Message, http.StatusBadRequest)
case *NotFoundError:
http.Error(w, err.Error(), http.StatusNotFound)
default:
log.Printf("unexpected error: %v", err)
http.Error(w, "internal error", http.StatusInternalServerError)
}
Panic & Recover
Panic
// Panic for truly unrecoverable situations
func MustCompile(pattern string) *Regexp {
re, err := Compile(pattern)
if err != nil {
panic("regexp: Compile(" + pattern + "): " + err.Error())
}
return re
}
| Panic rule | Detail |
|---|---|
| Only for programmer errors | Impossible states, violated invariants |
| Never for expected failures | Network errors, file not found → return error |
Must prefix convention |
MustCompile, MustParse — panics on error |
Acceptable in init() |
If package cannot initialize |
Recover
func safeHandler(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic recovered: %v\n%s", err, debug.Stack())
http.Error(w, "internal error", http.StatusInternalServerError)
}
}()
handleRequest(w, r)
}
| Recover rule | Detail |
|---|---|
| Only in deferred functions | recover() returns nil outside defer |
| Stops unwinding | Returns panic value |
| Use at goroutine/request boundary | Prevent one failure from crashing server |
| Always log the panic | With stack trace (debug.Stack()) |
| Don't recover to hide bugs | Fix the root cause |
Common Mistakes
| Mistake | Why Bad | Fix |
|---|---|---|
_ = f.Close() |
Silently loses write errors | if err := f.Close(); err != nil { ... } |
panic for expected errors |
Crashes program | Return error |
| Not wrapping errors | Lost context | fmt.Errorf("context: %w", err) |
Comparing errors with == |
Doesn't check wrapped chain | errors.Is(err, target) |
| Type-asserting errors directly | Doesn't check wrapped chain | errors.As(err, &target) |
Wrapping with %v when %w intended |
Breaks errors.Is / errors.As |
Use %w for wrapping |
| Error strings starting with capital | Convention violation | Lowercase: "opening file: ..." |
| Error strings ending with punctuation | Doesn't compose well | No period: "not found" not "not found." |
if err != nil { return err } everywhere |
Lost context | Add wrapping message |
| Returning error AND valid value | Confusing API | If err != nil, zero-value the result |
More from peixotorms/odinlayer-skills
elementor-development
Use when building Elementor addons, creating custom widgets, or managing Elementor components. Covers Widget_Base class (get_name, get_title, get_icon, register_controls, render, content_template), widget registration via elementor/widgets/register hook, addon structure and plugin header, wp_enqueue_script for widget assets, get_script_depends, get_style_depends, inline editing toolbars, custom widget categories, manager registration (register/unregister), selector tokens ({{WRAPPER}}), deprecation handling, and Elementor CLI commands.
65elementor-hooks
Use when hooking into Elementor lifecycle events, injecting controls, filtering widget output, or using the JS APIs. Covers elementor/init, elementor/element/before_section_end, elementor/element/after_section_end, elementor/widget/render_content filter, elementor/frontend/after_enqueue_styles, frontend JS hooks (elementorFrontend.hooks, frontend/element_ready), editor JS hooks (elementor.hooks), $e.commands API ($e.run, $e.commands.register), $e.routes, $e.hooks (registerUIBefore, registerUIAfter), control injection patterns, CSS file hooks, forms hooks (Pro), and query filters.
26elementor-themes
Use when building Elementor-compatible themes, registering theme locations, creating dynamic tags, or extending the Finder. Covers register_location, theme_builder locations, elementor_theme_do_location, Theme_Document and theme conditions, Tag_Base for dynamic tags (register_tag, get_value, render), Finder extension (Category_Base, register via elementor/finder/register), Context_Menu customization (elements/context-menu/groups filter), Hello Elementor theme (elementor-hello-theme, hello_elementor_* filters), and hosting page cache integration hooks.
25elementor-controls
Use when adding controls to Elementor widgets, creating custom controls, or referencing control type parameters. Covers add_control with types (TEXT, SELECT, SLIDER, COLOR, MEDIA, REPEATER, CHOOSE, NUMBER, SWITCHER, URL, ICONS), TYPOGRAPHY and BACKGROUND group controls, BORDER, BOX_SHADOW group controls, add_responsive_control, add_group_control, CSS selectors ({{WRAPPER}}, {{VALUE}}), condition and conditions for conditional display, dynamic content tags, POPOVER_TOGGLE, and global styles integration.
16elementor-forms
Use when creating custom Elementor form actions, custom form field types, form validation, or processing form submissions. Covers Elementor Pro forms (ElementorPro\Modules\Forms), Action_Base (get_name, get_label, run, register_settings_section, on_export), after_submit processing, Field_Base (field_type, render field HTML, validation callback, update_controls), content_template for editor preview, form action registration, export_type handling, update_record patterns, elementor_pro/forms/validation hook, email filters, and webhook response handling.
12flyonui
Use when building with FlyonUI — Tailwind CSS component library with CSS classes and optional JS plugins. Covers CSS component classes, JS plugin system (accordion, carousel, collapse, combobox, datatable, dropdown, select, tabs, tooltip, etc.), theming, installation, class reference, and plugin initialization via MCP tools.
9