Skip to content

Commit

Permalink
Merge pull request #4 from dsnet/josh/api-rethink
Browse files Browse the repository at this point in the history
Rework the API
  • Loading branch information
josharian committed May 5, 2022
2 parents 7b083f8 + b81ef8d commit c50f7b2
Show file tree
Hide file tree
Showing 2 changed files with 202 additions and 38 deletions.
150 changes: 125 additions & 25 deletions try.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// Example usage:
//
// func Fizz(...) (..., err error) {
// defer try.Catch(&err, func() {
// defer try.HandleF(&err, func() {
// if err == io.EOF {
// err = io.ErrUnexpectedEOF
// }
Expand All @@ -17,12 +17,13 @@
// return ..., nil
// }
//
// This package is not intended for production critical code as quick and easy
// error handling can occlude critical error handling logic.
// Rather, it is intended for short Go programs and unit tests where
// 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,
// calling Catch in a deferred function is optional.
// Since the E functions panic if an error is encountered, recovering in such programs is optional.
//
//
// Code before try:
Expand Down Expand Up @@ -57,7 +58,7 @@
// Code after try:
//
// func (a *MixedArray) UnmarshalNext(uo json.UnmarshalOptions, d *json.Decoder) (err error) {
// defer try.Catch(&err)
// defer try.Handle(&err)
// if t := try.E1(d.ReadToken()); t.Kind() != '[' {
// return fmt.Errorf("found %v, expecting array start", t.Kind())
// }
Expand All @@ -70,62 +71,161 @@
// return nil
// }
//
//
// Quick tour of the API
//
//
// The E family of functions all remove a final error return, panicking if non-nil.
//
//
// Handle allows easy assignment of that error to a return error value.
//
// func f() (err error) {
// defer try.Handle(&err)
// // ...
// }
//
//
// HandleF is like Handle, but it calls a function after any such assignment.
//
// func f() (err error) {
// defer try.HandleF(&err, func() {
// if err == io.EOF {
// err = io.ErrUnexpectedEOF
// }
// })
// // ...
// }
//
//
// F wraps an error with file and line formation and calls a function on error.
// It plays nicely with testing.TB and log.Fatal.
//
// func TestFoo(t *testing.T) {
// defer try.F(t.Fatal)
// // ...
// }
//
// func main() {
// defer try.F(log.Fatal)
// // ...
// }
//
//
// Recover is like F, but it supports more complicated error handling
// by passing the error and runtime frame directly to a function.
//
// func f() {
// defer try.Recover(func(err error, frame runtime.Frame) {
// // do something useful with err and frame
// })
// // ...
// }
//
//
package try

import (
"runtime"
"strconv"
)

// wrapError wraps an error to ensure that we only recover from errors
// panicked by this package.
type wrapError struct{ error }
type wrapError struct {
error
frame runtime.Frame
}

func (e wrapError) Error() string {
return e.frame.File + ":" + strconv.Itoa(e.frame.Line) + ": " + e.error.Error()
}

// Unwrap primarily exists for testing purposes.
func (e wrapError) Unwrap() error { return e.error }
func (e wrapError) Unwrap() error {
return e.error
}

// Catch catches a previously panicked error and stores it into err.
// If it successfully catches an error, it calls any provided handlers.
func Catch(err *error, handlers ...func()) {
switch ex := recover().(type) {
func r(recovered any, fn func(wrapError)) {
switch ex := recovered.(type) {
case nil:
return
case wrapError:
*err = ex.error
for _, handler := range handlers {
handler()
}
fn(ex)
default:
panic(ex)
}
}

// E panics if err is non-nil.
func E(err error) {
// Recover recovers an error previously panicked with an E function.
// If it recovers an error, it calls fn with the error and the runtime frame in which it occurred.
func Recover(fn func(err error, frame runtime.Frame)) {
r(recover(), func(w wrapError) { fn(w.error, w.frame) })
}

// Handle recovers an error previously panicked with an E function and stores it into errptr.
func Handle(errptr *error) {
r(recover(), func(w wrapError) { *errptr = w.error })
}

// HandleF recovers an error previously panicked with an E function and stores it into errptr.
// If it recovers an error, it calls fn.
func HandleF(errptr *error, fn func()) {
r(recover(), func(w wrapError) {
*errptr = w.error
if w.error != nil {
fn()
}
})
}

// F recovers an error previously panicked with an E function, wraps it, and passes it to fn.
// The wrapping includes the file and line of the runtime frame in which it occurred.
// F pairs well with testing.TB.Fatal and log.Fatal.
func F(fn func(...any)) {
r(recover(), func(w wrapError) { fn(w) })
}

func e(err error) {
if err != nil {
panic(wrapError{err})
pc := make([]uintptr, 1)
// 3: runtime.Callers, e, E
n := runtime.Callers(3, pc)
pc = pc[:n]
frames := runtime.CallersFrames(pc)
frame, _ := frames.Next()
panic(wrapError{error: err, frame: frame})
}
}

// E panics if err is non-nil.
func E(err error) {
e(err)
}

// E1 returns a as is.
// It panics if err is non-nil.
func E1[A any](a A, err error) A {
E(err)
e(err)
return a
}

// E2 returns a and b as is.
// It panics if err is non-nil.
func E2[A, B any](a A, b B, err error) (A, B) {
E(err)
e(err)
return a, b
}

// E3 returns a, b, and c as is.
// It panics if err is non-nil.
func E3[A, B, C any](a A, b B, c C, err error) (A, B, C) {
E(err)
e(err)
return a, b, c
}

// E4 returns a, b, c, and d as is.
// It panics if err is non-nil.
func E4[A, B, C, D any](a A, b B, c C, d D, err error) (A, B, C, D) {
E(err)
e(err)
return a, b, c, d
}
90 changes: 77 additions & 13 deletions try_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ package try_test
import (
"errors"
"io"
"log"
"runtime"
"strings"
"testing"

"github.com/dsnet/try"
Expand All @@ -19,7 +22,7 @@ func Test(t *testing.T) {
wantError error
wantPanic error
}{{
name: "NoCatch/Success",
name: "NoRecover/Success",
run: func(t *testing.T) error {
a, b, c := try.E3(success())
if a != 1 && b != "success" && c != true {
Expand All @@ -28,36 +31,36 @@ func Test(t *testing.T) {
return nil
},
}, {
name: "NoCatch/Failure",
name: "NoRecover/Failure",
run: func(t *testing.T) error {
a, b, c := try.E3(failure())
t.Errorf("failure() = (%v, %v, %v), want panic", a, b, c)
return nil
},
wantPanic: io.EOF,
}, {
name: "Catch/Success",
name: "Recover/Success",
run: func(t *testing.T) (err error) {
defer try.Catch(&err)
defer try.Handle(&err)
a, b, c := try.E3(success())
if a != 1 && b != "success" && c != true {
t.Errorf("success() = (%v, %v, %v), want (1, success, true)", a, b, c)
}
return nil
},
}, {
name: "Catch/Failure",
name: "Recover/Failure",
run: func(t *testing.T) (err error) {
defer try.Catch(&err)
defer try.Handle(&err)
a, b, c := try.E3(failure())
t.Errorf("failure() = (%v, %v, %v), want panic", a, b, c)
return nil
},
wantError: io.EOF,
}, {
name: "Catch/Failure/Ignored",
name: "Recover/Failure/Ignored",
run: func(t *testing.T) (err error) {
defer try.Catch(&err, func() {
defer try.HandleF(&err, func() {
if err == io.EOF {
err = nil
}
Expand All @@ -67,9 +70,9 @@ func Test(t *testing.T) {
return nil
},
}, {
name: "Catch/Failure/Replaced",
name: "Recover/Failure/Replaced",
run: func(t *testing.T) (err error) {
defer try.Catch(&err, func() {
defer try.HandleF(&err, func() {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
Expand All @@ -85,7 +88,17 @@ func Test(t *testing.T) {
var gotError error
var gotPanic error
func() {
defer func() { gotPanic, _ = recover().(error) }()
defer func() {
r := recover()
if r == nil {
return
}
var ok bool
gotPanic, ok = r.(error)
if !ok {
t.Errorf("recovered non-error %T", r)
}
}()
gotError = tt.run(t)
}()
switch {
Expand All @@ -98,6 +111,57 @@ func Test(t *testing.T) {
}
}

func TestFrame(t *testing.T) {
t.Run("E", func(t *testing.T) {
defer try.Recover(func(err error, frame runtime.Frame) {
if frame.File != "x.go" {
t.Errorf("want File=x.go, got %q", frame.File)
}
if frame.Line != 4 {
t.Errorf("want Line=4, got %d", frame.Line)
}
})
//line x.go:4
try.E(errors.New("crash and burn"))
})
t.Run("E3", func(t *testing.T) {
defer try.Recover(func(err error, frame runtime.Frame) {
if frame.File != "x.go" {
t.Errorf("want File=x.go, got %q", frame.File)
}
if frame.Line != 4 {
t.Errorf("want Line=4, got %d", frame.Line)
}
})
//line x.go:4
try.E3(failure())
})
}

func TestF(t *testing.T) {
buf := new(strings.Builder)
logger := log.New(buf, "", 0)
defer func() {
const want = "y.go:10: EOF\n"
if got := buf.String(); got != want {
t.Errorf("want %q, got %q", want, got)
}
}()
defer try.F(logger.Print)
//line y.go:10
try.E(io.EOF)
}

func TestHandleOverwrite(t *testing.T) {
err := func() (err error) {
try.Handle(&err)
return io.EOF
}()
if err !=io.EOF {
t.Errorf("want %v, got %v", err, io.EOF)
}
}

func success() (a int, b string, c bool, err error) {
return +1, "success", true, nil
}
Expand All @@ -110,7 +174,7 @@ func BenchmarkSuccess(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
func() (err error) {
defer try.Catch(&err)
defer try.Handle(&err)
try.E3(success())
return nil
}()
Expand All @@ -121,7 +185,7 @@ func BenchmarkFailure(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
func() (err error) {
defer try.Catch(&err)
defer try.Handle(&err)
try.E3(failure())
return nil
}()
Expand Down

0 comments on commit c50f7b2

Please sign in to comment.