try

package module
v0.0.0-...-ea4fcd0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 29, 2026 License: BSD-3-Clause Imports: 5 Imported by: 0

README

Try: Simplified Error Handling in Go

GoDev Build Status

This module reduces the syntactic cost of error handling in Go.

Example usage in a main program:

func main() {
    defer try.Forward(log.Fatal)
    b := try.E1(os.ReadFile(...))
    var v any
    try.E(json.Unmarshal(b, &v))
    ...
}

Example usage in a unit test:

func Test(t *testing.T) {
    defer try.Forward(t.Fatal)
    db := try.E1(setdb.Open(...))
    defer db.Close()
    ...
    try.E(db.Commit())
}

Code before try:

func (a *MixedArray) UnmarshalNext(uo json.UnmarshalOptions, d *json.Decoder) error {
    switch t, err := d.ReadToken(); {
    case err != nil:
        return err
    case t.Kind() != '[':
        return fmt.Errorf("got %v, expecting array start", t.Kind())
    }

    if err := uo.UnmarshalNext(d, &a.Scalar); err != nil {
        return err
    }
    if err := uo.UnmarshalNext(d, &a.Slice); err != nil {
        return err
    }
    if err := uo.UnmarshalNext(d, &a.Map); err != nil {
        return err
    }

    switch t, err := d.ReadToken(); {
    case err != nil:
        return err
    case t.Kind() != ']':
        return fmt.Errorf("got %v, expecting array end", t.Kind())
    }
    return nil
}

Code after try:

func (a *MixedArray) UnmarshalNext(uo json.UnmarshalOptions, d *json.Decoder) (err error) {
    defer try.Handle(&err)
    if t := try.E1(d.ReadToken()); t.Kind() != '[' {
        return fmt.Errorf("found %v, expecting array start", t.Kind())
    }
    try.E(uo.UnmarshalNext(d, &a.Scalar))
    try.E(uo.UnmarshalNext(d, &a.Slice))
    try.E(uo.UnmarshalNext(d, &a.Map))
    if t := try.E1(d.ReadToken()); t.Kind() != ']' {
        return fmt.Errorf("found %v, expecting array end", t.Kind())
    }
    return nil
}

Stack traces

Every try.E* that observes a non-nil error raises a *try.TryError: an ordinary error that also captures a stack trace from where it was raised. It implements the standard interfaces, so the trace travels with the error:

func LoadConfig(path string) (cfg Config, err error) {
    defer try.Handle(&err, func(err error) error {
        return fmt.Errorf("load config %q: %w", path, err)
    })
    b := try.E1(os.ReadFile(path))
    try.E(json.Unmarshal(b, &cfg))
    return cfg, nil
}
  • errors.Is(err, fs.ErrNotExist) / errors.As(err, new(*try.TryError)) see through it.
  • fmt.Printf("%+v", err) prints the message followed by the full stack trace.
  • json.Marshal(err) emits {"error": ..., "stack": ["function:file:line", ...]}.

See the documentation for more information.

Install

go get -u github.com/dsnet/try

Semgrep rules

These semgrep rules can help prevent bugs and abuse:

rules:
  - id: non-deferred-try-handle
    patterns:
      - pattern-either:
          - pattern: try.Forward(...)
          - pattern: try.Handle(...)
          - pattern: try.Recover(...)
      - pattern-not: defer try.Forward(...)
      - pattern-not: defer try.Handle(...)
      - pattern-not: defer try.Recover(...)
    message: Calls to try handlers must be deferred
    severity: ERROR
    languages:
      - go
  - id: missing-try-handler
    patterns:
      - pattern-either:
          - pattern: try.E(...)
          - pattern: try.E1(...)
          - pattern: try.E2(...)
          - pattern: try.E3(...)
          - pattern: try.E4(...)
      - pattern-not-inside: |
          ...
          defer try.Forward(...)
          ...
      - pattern-not-inside: |
          ...
          defer try.Handle(...)
          ...
      - pattern-not-inside: |
          ...
          defer try.Recover(...)
          ...
    message: Calls to try.E[n] must have a matching function-local handler
    severity: ERROR
    languages:
      - go

License

BSD - See LICENSE file

Documentation

Overview

Package try emulates aspects of the ill-fated "try" proposal using generics. See https://golang.org/issue/32437 for inspiration.

The model is two verbs:

  • The E family of functions raise an error: they strip a final error return, panicking with a TryError if it is non-nil.
  • A deferred handler catches it: Handle stores it into a returned error, Recover passes it to a func(error), and Forward passes it to a func(...any) sink such as log.Fatal or testing.TB.Fatal.

Example usage:

func Fizz(...) (..., err error) {
	defer try.Handle(&err, func(err error) error {
		if err == io.EOF {
			return io.ErrUnexpectedEOF
		}
		return err
	})
	... := try.E2(Buzz(...))
	return ..., nil
}

This package is a sharp tool and should be used with care. Quick and easy error handling can occlude critical error handling logic. Panic handling generally should not cross package boundaries or be an explicit part of an API.

Package try is a good fit for short Go programs and unit tests where development speed is a greater priority than reliability. Since the E functions panic if an error is encountered, recovering in such programs is optional.

Code before try:

func (a *MixedArray) UnmarshalNext(uo json.UnmarshalOptions, d *json.Decoder) error {
	switch t, err := d.ReadToken(); {
	case err != nil:
		return err
	case t.Kind() != '[':
		return fmt.Errorf("got %v, expecting array start", t.Kind())
	}

	if err := uo.UnmarshalNext(d, &a.Scalar); err != nil {
		return err
	}
	if err := uo.UnmarshalNext(d, &a.Slice); err != nil {
		return err
	}
	if err := uo.UnmarshalNext(d, &a.Map); err != nil {
		return err
	}

	switch t, err := d.ReadToken(); {
	case err != nil:
		return err
	case t.Kind() != ']':
		return fmt.Errorf("got %v, expecting array end", t.Kind())
	}
	return nil
}

Code after try:

func (a *MixedArray) UnmarshalNext(uo json.UnmarshalOptions, d *json.Decoder) (err error) {
	defer try.Handle(&err)
	if t := try.E1(d.ReadToken()); t.Kind() != '[' {
		return fmt.Errorf("found %v, expecting array start", t.Kind())
	}
	try.E(uo.UnmarshalNext(d, &a.Scalar))
	try.E(uo.UnmarshalNext(d, &a.Slice))
	try.E(uo.UnmarshalNext(d, &a.Map))
	if t := try.E1(d.ReadToken()); t.Kind() != ']' {
		return fmt.Errorf("found %v, expecting array end", t.Kind())
	}
	return nil
}

Quick tour of the API

The E family of functions all remove a final error return, panicking with a TryError if it is non-nil. The TryError captures a stack trace at the point it was raised. It is an ordinary error: errors.Is and errors.As see through it, fmt's "%+v" verb prints the stack trace, and it marshals to JSON with the trace included.

Handle recovers from that panic and allows assignment of the error to a return error value, optionally wrapping it. Other panics are not recovered.

func f() (err error) {
	defer try.Handle(&err)
	...
}

Handle accepts wrap functions that are applied in order, e.g. to add context:

func foo(i int) (err error) {
	defer try.Handle(&err, func(err error) error {
		return fmt.Errorf("unable to foo %d: %w", i, err)
	})
	...
}

Forward passes the error to fn, wrapped with file and line information. It inter-operates well with testing.TB and log.Fatal.

func TestFoo(t *testing.T) {
	defer try.Forward(t.Fatal)
	...
}

func main() {
	defer try.Forward(log.Fatal)
	...
}

Recover is like Forward, but it passes the error to a func(error). The error carries the stack trace, accessible via errors.As or "%+v".

func f() {
	defer try.Recover(func(err error) {
		var te *try.TryError
		if errors.As(err, &te) {
			// do something useful with te.StackTrace()
		}
	})
	...
}

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func E

func E(err error)

E panics if err is non-nil.

func E1

func E1[A any](a A, err error) A

E1 returns a as is. It panics if err is non-nil.

func E2

func E2[A, B any](a A, b B, err error) (A, B)

E2 returns a and b as is. It panics if err is non-nil.

func E3

func E3[A, B, C any](a A, b B, c C, err error) (A, B, C)

E3 returns a, b, and c as is. It panics if err is non-nil.

func E4

func E4[A, B, C, D any](a A, b B, c C, d D, err error) (A, B, C, D)

E4 returns a, b, c, and d as is. It panics if err is non-nil.

func Forward

func Forward(fn func(...any))

Forward recovers an error previously raised with an E function and passes it to fn. The error formats with the file and line of the frame in which it occurred. Forward pairs well with testing.TB.Fatal and log.Fatal, which are func(...any) and so cannot be passed to Recover.

func Handle

func Handle(errptr *error, wrap ...func(error) error)

Handle recovers an error previously raised with an E function and stores it into errptr. Any wrap functions are applied in order before storing, e.g. to add context. The stored error chain still contains the *TryError, so the captured stack survives for callers using errors.As or "%+v".

func Recover

func Recover(fn func(err error))

Recover recovers an error previously raised with an E function and passes it to fn. The error carries the stack trace, accessible via errors.As or "%+v".

Types

type TryError

type TryError struct {
	// contains filtered or unexported fields
}

TryError is the error raised by the E family of functions and recovered by this package's handlers. It wraps an underlying error and captures the call stack at the point an E function observed a non-nil error.

TryError is exported so that callers up the stack can recover the trace via errors.As(err, new(*TryError)), the fmt "%+v" verb, or json.Marshal. It is the marker type this package uses to distinguish its own panics from foreign ones.

func (*TryError) Error

func (e *TryError) Error() string

Error returns "file:line: <message>", using the innermost captured frame.

func (*TryError) Format

func (e *TryError) Format(s fmt.State, verb rune)

Format implements fmt.Formatter:

%s, %v   "file:line: message"               (innermost frame, == Error)
%+v      message followed by the full stack  (pkg/errors convention)
%q       quoted %s

func (*TryError) MarshalJSON

func (e *TryError) MarshalJSON() ([]byte, error)

MarshalJSON marshals the error message together with the stack trace. Each frame is a single "function:file:line" string. Without this, an error marshals to "{}" since it has no exported fields.

func (*TryError) StackTrace

func (e *TryError) StackTrace() []runtime.Frame

StackTrace resolves and returns the captured frames, innermost first. Resolution is lazy: nothing is symbolized until this is called.

func (*TryError) Unwrap

func (e *TryError) Unwrap() error

Unwrap exposes the wrapped error so errors.Is and errors.As see through it.

Directories

Path Synopsis
Command example demonstrates the try package: the E functions raise errors, a deferred handler catches them, and the resulting *try.TryError carries a stack trace that survives as an ordinary error.
Command example demonstrates the try package: the E functions raise errors, a deferred handler catches them, and the resulting *try.TryError carries a stack trace that survives as an ordinary error.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL