-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
"try" semantics #2
Open
warpfork
wants to merge
6
commits into
spacemonkeygo:master
Choose a base branch
from
polydawn:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
a354658
Add 'try' system for scoped panic handling.
warpfork 6758c0d
Export try.Plan.
warpfork 918d306
Add support for catching & handling panics of mundane types, such as …
kofalt f313887
Add convenience function for repanicing and maintaining the original …
warpfork 491d8ee
Additional features for wrapped try errors.
warpfork 0b05cac
Package docs on appropriate use (as well as not).
warpfork File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a super terrible test. I could say I left it in so it could spark some discussion, but really, I just forgot it was here.
I'd love suggestions for how to get coverage or describe this behavior correctly... otherwise it should probably thrown away, because all it does is print stuff that has to be eyeballed, and that's just not a test.