diff --git a/try/try.go b/try/try.go new file mode 100644 index 0000000..3f38515 --- /dev/null +++ b/try/try.go @@ -0,0 +1,232 @@ +/* + `try` provides idioms for scoped panic handling. + + It's specifically aware of spacemonkey errors and can easily create + type-aware handling blocks. + + For a given block of code that is to be tried, several kinds of error + handling are possible: + - `Catch(type, func(err) {...your handler...})` + - `CatchAll(func(err) {...your handler...})` + - `Finally(func() {...your handler...})` + + `Catch` and `CatchAll` blocks consume the error -- it will not be re-raised + unless the handlers explicitly do so. `Finally` blocks run even in the + absense of errors (much like regular defers), and do not consume errors -- + they will be re-raised after the execution of the `Finally` block. + + Matching of errors occurs in order. This has a few implications: + - If using `Catch` blocks with errors that are subclasses of other errors + you're handling in the same sequence, put the most specific ones first. + - `CatchAll` blocks should be last (or they'll eat all errors, + even if you declare more `Catch` blocks later). + + `Finally` blocks will be run at the end of the error handling sequence + regardless of their declaration order. + + Additional panics from a `Catch` or `CatchAll` block will still cause + `Finally` blocks to be executed. However, note that additional panics + raised from any handler blocks will cause the original error to be masked + -- be careful of this. + + Panics with values that are not spacemonkey errors will be handled + (no special treatment; they'll hit `CatchAll` blocks and `Finally` blocks; + it would of course be silly for them to hit `Catch` blocks since those + use spacemonkey-errors types). Panics with values that are not of golang's + `error` type at all will be wrapped in a spacemonkey error for the purpose + of handling (as an interface design, `CatchAll(interface{})` is unpleasant). + See `OriginalError` and `Repanic` for more invormation on handling these + wrapped panic objects. + + A `try.Do(func() {...})` with no attached errors handlers is legal but + pointless. A `try.Do(func() {...})` with no `Done()` will never run the + function (which is good; you won't forget to call it). + + For spacemonkey errors, the 'exit' path will be automatically recorded for + each time the errors is rethrown. This is not a complete record of where + the error has been, and reexamining the current stack may give a more + complete picture. Note that 'finally' blocks will never be recorded + (unless of course they raise a new error!), since they are functions that + return normally. + + A note about use cases: while `try` should be familiar and comfortable + to users of exceptions in other languages, and we feel use of a "typed" + panic mechanism results in effective error handling with a minimization + of boilerplate checking error return codes... `try` and `panic` are not + always the correct answer! There are at least two situations where you + may want to consider converting back to handling errors as regular values: + If passing errors between goroutines, of course you need to pass them + as values. Another situation where error returns are more capable than + panics is when part of multiple-returns, where the other values may have + been partially assembled and still may need be subject to cleanup by the + caller. Fortunately, you can always use `CatchAll` to easily fence a block + of panic-oriented code and convert it into errors-by-value flow. +*/ +package try + +import ( + "fmt" + + "github.com/spacemonkeygo/errors" +) + +var ( + // Panic type when a panic is caught that is neither a spacemonkey error, nor an ordinary golang error. + // For example, panic("hooray!") + UnknownPanicError = errors.NewClass("Unknown Error") + + // The spacemonkey error key to get the original data out of an UnknownPanicError. + OriginalErrorKey = errors.GenSym() +) + +type Plan struct { + main func() + catch []check + finally func() +} + +type check struct { + match *errors.ErrorClass + handler func(err *errors.Error) + anyhandler func(err error) +} + +func Do(f func()) *Plan { + return &Plan{main: f, finally: func() {}} +} + +func (p *Plan) Catch(kind *errors.ErrorClass, handler func(err *errors.Error)) *Plan { + p.catch = append(p.catch, check{ + match: kind, + handler: handler, + }) + return p +} + +func (p *Plan) CatchAll(handler func(err error)) *Plan { + p.catch = append(p.catch, check{ + match: nil, + anyhandler: handler, + }) + return p +} + +func (p *Plan) Finally(f func()) *Plan { + f2 := p.finally + p.finally = func() { + f() + f2() + } + return p +} + +func (p *Plan) Done() { + defer func() { + rec := recover() + consumed := false + defer func() { + p.finally() + if !consumed { + panic(rec) + } + }() + switch err := rec.(type) { + case nil: + consumed = true + return + case *errors.Error: + // record the origin location of the error. + // this is redundant at first, but useful if the error is rethrown; + // then it shows line of the panic that rethrew it. + errors.RecordBefore(err, 3) + // run all checks + for _, catch := range p.catch { + if catch.match == nil { + consumed = true + catch.anyhandler(err) + return + } + if err.Is(catch.match) { + consumed = true + catch.handler(err) + return + } + } + case error: + // grabbag error, so skip all the typed catches, but still do wildcards and finally. + for _, catch := range p.catch { + if catch.match == nil { + consumed = true + catch.anyhandler(err) + return + } + } + default: + // handle the case where it's not even an error type. + // we'll wrap your panic in an UnknownPanicError and add the original as data for later retrieval. + for _, catch := range p.catch { + if catch.match == nil { + consumed = true + msg := fmt.Sprintf("%v", rec) + pan := UnknownPanicError.NewWith(msg, errors.SetData(OriginalErrorKey, rec)) + catch.anyhandler(pan) + return + } + if UnknownPanicError.Is(catch.match) { + consumed = true + msg := fmt.Sprintf("%v", rec) + pan := UnknownPanicError.NewWith(msg, errors.SetData(OriginalErrorKey, rec)) + catch.handler(pan.(*errors.Error)) + return + } + } + } + }() + p.main() +} + +/* + If `err` was originally another value coerced to an error by `CatchAll`, + this will return the original value. Otherwise, it returns the same error + again. + + See also `Repanic()`. +*/ +func OriginalError(err error) interface{} { + data := errors.GetData(err, OriginalErrorKey) + if data == nil { + return err + } + return data +} + +/* + Panics again with the original error. + + Shorthand, equivalent to calling `panic(OriginalError(err))` + + If the error is a `UnknownPanicError` (i.e., a `CatchAll` block that's handling something + that wasn't originally an `error` type, so it was wrapped), it will unwrap that re-panic + with that original error -- in other words, this is a "do the right thing" method in all scenarios. + + You may simply `panic(err)` for all other typed `Catch` blocks (since they are never wraps), + though there's no harm in using `Repanic` consistently if you prefer. It is also safe to + use `Repanic` on other non-`error` types (though you're really not helping anyone with that + behavior) and objects that didn't originally come in as the handler's error. No part of + the error handling or finally handlers chain will change execution order as a result; + `panic` and `Repanic` cause identical behavior in that regard. +*/ +func Repanic(err error) { + wrapper, ok := err.(*errors.Error) + if !ok { + panic(err) + } + if !wrapper.Is(UnknownPanicError) { + panic(err) + } + data := errors.GetData(err, OriginalErrorKey) + if data == nil { + panic(errors.ProgrammerError.New("misuse of try internals", errors.SetData(OriginalErrorKey, err))) + } + panic(data) +} diff --git a/try/try_stack_test.go b/try/try_stack_test.go new file mode 100644 index 0000000..24c771b --- /dev/null +++ b/try/try_stack_test.go @@ -0,0 +1,54 @@ +package try_test + +import ( + "fmt" + "path/filepath" + "runtime" + "testing" + + "github.com/spacemonkeygo/errors" + "github.com/spacemonkeygo/errors/try" +) + +func TestStackHandling(t *testing.T) { + // Crappy test. Try harder later. + try.Do(func() { + try.Do(func() { + fmt.Println("function called") + panic(AppleError.New("emsg")) + }).Finally(func() { + fmt.Println("finally block called") + }).Catch(FruitError, func(e *errors.Error) { + fmt.Println("fruit handler called") + panic(e) + }).Done() + }).CatchAll(func(e error) { + fmt.Println("exit route:") + fmt.Println(errors.GetExits(e)) + fmt.Println() + fmt.Println("recorded stack:") + fmt.Println(errors.GetStack(e)) + fmt.Println() + + fmt.Println("final stack:") + var pcs [256]uintptr + amount := runtime.Callers(3, pcs[:]) + for i := 0; i < amount; i++ { + fmt.Println(frameStringer(pcs[i])) + } + fmt.Println() + + }).Done() +} + +func frameStringer(pc uintptr) string { + if pc == 0 { + return "unknown.unknown:0" + } + f := runtime.FuncForPC(pc) + if f == nil { + return "unknown.unknown:0" + } + file, line := f.FileLine(pc) + return fmt.Sprintf("%s:%s:%d", f.Name(), filepath.Base(file), line) +} diff --git a/try/try_test.go b/try/try_test.go new file mode 100644 index 0000000..797c2b6 --- /dev/null +++ b/try/try_test.go @@ -0,0 +1,328 @@ +package try_test + +import ( + "fmt" + + "github.com/spacemonkeygo/errors" + "github.com/spacemonkeygo/errors/try" +) + +func ExampleNormalFlow() { + try.Do(func() { + fmt.Println("function called") + }).Finally(func() { + fmt.Println("finally block called") + }).CatchAll(func(_ error) { + fmt.Println("catch wildcard called") + }).Done() + + // Output: + // function called + // finally block called +} + +func ExampleErrorInTry() { + try.Do(func() { + fmt.Println("function called") + panic(fmt.Errorf("any error")) + }).Finally(func() { + fmt.Println("finally block called") + }).CatchAll(func(_ error) { + fmt.Println("catch wildcard called") + }).Done() + + // Output: + // function called + // catch wildcard called + // finally block called +} + +func ExampleCrashInCatch() { + try.Do(func() { + try.Do(func() { + fmt.Println("function called") + panic(fmt.Errorf("any error")) + }).Finally(func() { + fmt.Println("finally block called") + }).CatchAll(func(_ error) { + fmt.Println("catch wildcard called") + panic(fmt.Errorf("zomg")) + }).Done() + }).CatchAll(func(e error) { + fmt.Println("outer error caught:", e.Error()) + }).Done() + + // Output: + // function called + // catch wildcard called + // finally block called + // outer error caught: zomg +} + +func ExampleErrorsLeaveFinally() { + try.Do(func() { + try.Do(func() { + fmt.Println("function called") + panic(fmt.Errorf("inner error")) + }).Finally(func() { + fmt.Println("finally block called") + }).Done() + }).CatchAll(func(e error) { + fmt.Println("outer error caught:", e.Error()) + }).Done() + + // Output: + // function called + // finally block called + // outer error caught: inner error +} + +func ExampleCrashInFinally() { + try.Do(func() { + try.Do(func() { + fmt.Println("function called") + }).Finally(func() { + fmt.Println("finally block called") + panic(fmt.Errorf("zomg")) + }).CatchAll(func(_ error) { + fmt.Println("catch wildcard called") + }).Done() + }).CatchAll(func(e error) { + fmt.Println("outer error caught:", e.Error()) + }).Done() + + // Output: + // function called + // finally block called + // outer error caught: zomg +} + +var FruitError = errors.NewClass("fruit") +var AppleError = FruitError.NewClass("apple") +var GrapeError = FruitError.NewClass("grape") +var RockError = errors.NewClass("rock") + +func ExampleCatchingErrorsByType() { + try.Do(func() { + try.Do(func() { + fmt.Println("function called") + panic(AppleError.New("emsg")) + }).Finally(func() { + fmt.Println("finally block called") + }).Catch(RockError, func(e *errors.Error) { + fmt.Println("rock handler called") + }).Catch(FruitError, func(e *errors.Error) { + fmt.Println("fruit handler called") + }).CatchAll(func(_ error) { + fmt.Println("catch wildcard called") + }).Done() + }).CatchAll(func(e error) { + fmt.Println("outer error caught:", e.Error()) + }).Done() + + // Output: + // function called + // fruit handler called + // finally block called +} + +func ExampleCatchingErrorsBySpecificType() { + try.Do(func() { + try.Do(func() { + fmt.Println("function called") + panic(AppleError.New("emsg")) + }).Finally(func() { + fmt.Println("finally block called") + }).Catch(AppleError, func(e *errors.Error) { + fmt.Println("apple handler called") + }).Catch(FruitError, func(e *errors.Error) { + fmt.Println("fruit handler called") + }).CatchAll(func(_ error) { + fmt.Println("catch wildcard called") + }).Done() + }).CatchAll(func(e error) { + fmt.Println("outer error caught:", e.Error()) + }).Done() + + // Output: + // function called + // apple handler called + // finally block called +} + +func ExampleIntPanic() { + try.Do(func() { + fmt.Println("function called") + panic(3) + }).Finally(func() { + fmt.Println("finally block called") + }).CatchAll(func(_ error) { + fmt.Println("catch wildcard called") + }).Done() + + // Output: + // function called + // catch wildcard called + // finally block called +} + +func ExampleIntOriginalPanic() { + try.Do(func() { + fmt.Println("function called") + panic(3) + }).Finally(func() { + fmt.Println("finally block called") + }).CatchAll(func(e error) { + data := errors.GetData(e, try.OriginalErrorKey) + fmt.Println("catch wildcard called:", data) + switch data.(type) { + case int: + fmt.Println("type is int") + } + }).Done() + + // Output: + // function called + // catch wildcard called: 3 + // type is int + // finally block called +} + +func ExampleCatchingUnknownErrorsByType() { + try.Do(func() { + fmt.Println("function called") + panic(3) + }).Finally(func() { + fmt.Println("finally block called") + }).Catch(RockError, func(e *errors.Error) { + fmt.Println("catch a rock") + }).Catch(try.UnknownPanicError, func(e *errors.Error) { + data := errors.GetData(e, try.OriginalErrorKey) + fmt.Println("catch UnknownPanicError called:", data) + switch data.(type) { + case int: + fmt.Println("type is int") + } + }).CatchAll(func(e error) { + fmt.Println("catch wildcard called") + }).Done() + + // Output: + // function called + // catch UnknownPanicError called: 3 + // type is int + // finally block called +} + +func ExampleStringPanic() { + try.Do(func() { + fmt.Println("function called") + panic("hey") + }).Finally(func() { + fmt.Println("finally block called") + }).CatchAll(func(_ error) { + fmt.Println("catch wildcard called") + }).Done() + + // Output: + // function called + // catch wildcard called + // finally block called +} + +func ExampleStringEscalatingPanic() { + try.Do(func() { + try.Do(func() { + fmt.Println("function called") + panic("hey") + }).Finally(func() { + fmt.Println("finally block called") + }).Done() + }).CatchAll(func(e error) { + fmt.Println("outer error caught") + }).Done() + + // Output: + // function called + // finally block called + // outer error caught +} + +func ExampleStringCrashInFinally() { + try.Do(func() { + try.Do(func() { + fmt.Println("function called") + }).Finally(func() { + fmt.Println("finally block called") + panic("hey") + }).CatchAll(func(_ error) { + fmt.Println("catch wildcard called") + }).Done() + }).CatchAll(func(e error) { + fmt.Println("outer error caught") + }).Done() + + // Output: + // function called + // finally block called + // outer error caught +} + +func ExampleStringRepanicCoversFinally() { + try.Do(func() { + try.Do(func() { + fmt.Println("function called") + panic("hey") + }).Finally(func() { + fmt.Println("finally block called") + }).Catch(try.UnknownPanicError, func(e *errors.Error) { + data := errors.GetData(e, try.OriginalErrorKey) + fmt.Println("catch UnknownPanicError called:", data) + switch data.(type) { + case string: + fmt.Println("type is string") + } + + panic(data) + }).CatchAll(func(_ error) { + fmt.Println("catch wildcard called") + }).Done() + }).CatchAll(func(e error) { + fmt.Println("outer error caught") + }).Done() + + // Output: + // function called + // catch UnknownPanicError called: hey + // type is string + // finally block called + // outer error caught +} + +// this is a particularly useful pattern for doing cleanup on error, but not on success. +func ExampleObjectRepanicOriginal() { + obj := struct{}{} + try.Do(func() { + try.Do(func() { + fmt.Println("function called") + panic(obj) + }).Finally(func() { + fmt.Println("finally block called") + }).CatchAll(func(e error) { + fmt.Println("catch wildcard called") + // repanic... with the original error! + try.Repanic(e) + }).Done() + }).CatchAll(func(e error) { + // this example is a little funny, because it got re-wrapped again + // but the important part is yes, the (pointer equal!) object is in there. + data := errors.GetData(e, try.OriginalErrorKey) + fmt.Printf("outer error equals original: %v\n", data == obj) + }).Done() + + // Output: + // function called + // catch wildcard called + // finally block called + // outer error equals original: true +}