Skip to content

Commit 7e5f73e

Browse files
committed
Allow to contain stack trace in deescalated panic's error. Remove functions which promote direct access of os.Stderr
1 parent eb341ab commit 7e5f73e

File tree

5 files changed

+69
-61
lines changed

5 files changed

+69
-61
lines changed

CHANGES.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@
1414
- Major rewrite.
1515

1616
# v0.4.0 (2020-02-07)
17-
- Add more info to README.md.
17+
- Added more info to README.md.
1818
- Removed `Handle()`.
1919
- `panik.value` is now `panik.Value`, allowing users to inspect the value.
2020
- Changed signature of `Panic()` to be consistent with `panic()`.
2121
- Simplified API.
2222

2323
# v0.4.1 (2020-04-21)
24-
- Add `RecoverTraceFunc()` and `ExitTraceFunc()`.
24+
- Added `RecoverTraceFunc()` and `ExitTraceFunc()`.
25+
26+
# v0.5.0 (2020-05-12)
27+
- Removed `RecoverTrace()` and `ExitTrace()`.
28+
- Added `ToErrorWithTrace()`.

README.md

+22-11
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ The following functions only do something `if err != nil`, and act as a replacem
4747
func OnError(err error) {} // panics with an error which wraps err
4848
func OnErrore(err error, panicErr error) {} // panics with an error which wraps panicErr and returns fmt.Sprintf("%v: %v", panicErr, err) for Error()
4949
func OnErrorfw(err error, format string, args ...interface{}) {} // panics with an error which wraps err and returns fmt.Sprintf("%s: %v", fmt.Sprintf(format, args...), err) for Error()
50-
func OnErrorfv(err error, format string, args ...interface{}) {} // panics with an error which returns fmt.Sprintf("%s: %v", fmt.Sprintf(format, args...), err) for Error()
50+
func OnErrorfv(err error, format string, args ...interface{}) {} // panics with an error which does not wrap err and returns fmt.Sprintf("%s: %v", fmt.Sprintf(format, args...), err) for Error()
5151
```
5252

5353
### panic to panic with more information
@@ -63,13 +63,14 @@ Use `Wrap()` and `Wrapf()` with the `defer`-statement.
6363

6464
```go
6565
func ToError(retErr *error) {} // assigns result of recover() to *retErr
66+
func ToErrorWithTrace(retErr *error) {} // like ToError(), but also contains full stack trace in the error's message
6667
```
6768

68-
Use `ToError()` with the `defer`-statement.
69+
Use `ToError()` and `ToErrorWithTrace()` with the `defer`-statement.
6970

7071
#### an important footnote
7172

72-
To prevent unwanted recovery and deescalation of panics originating from programming errors, panik will never lastingly recover from panics created with `panic()`. Specifically, you need to use [one of panik's panicking functions](#error-to-panic) for `defer ToError(err)` to set `*err`.
73+
To prevent unwanted recovery and deescalation of panics originating from programming errors, panik will never lastingly recover from panics created with `panic()`. Specifically, you need to use [one of panik's panicking functions](#error-to-panic) for `defer ToError(&err)` to set `*err`.
7374

7475
### inspect recovered panic value
7576

@@ -80,15 +81,13 @@ func Caused(r interface{}) bool {} // returns true if r was recovered from a pan
8081
### print a stack trace
8182

8283
```go
83-
func RecoverTrace() {} // to stderr
8484
func RecoverTraceTo(w io.Writer) {} // to w
8585
func RecoverTraceFunc(f func(trace string)) {} // to whatever else, e.g. an error dialog box (convenience function)
86-
func ExitTrace() {} // like RecoverTrace(), followed by os.Exit(2)
8786
func ExitTraceTo(w io.Writer) {} // like RecoverTraceTo(), followed by os.Exit(2)
8887
func ExitTraceFunc(f func(trace string)) {} // like RecoverTraceFunc(), followed by os.Exit(2)
8988
```
9089

91-
Use `RecoverTrace`(`To`)`()` in libraries. Use `ExitTrace`(`To`)`()` in `main()`.
90+
Use `RecoverTrace`(`Func`)`()` in libraries. Use `ExitTrace`(`Func`)`()` in `main()`.
9291

9392
## A practical overview
9493

@@ -111,24 +110,36 @@ func getEverything() []interface{} {
111110
}
112111

113112
func GetEverythingAndThenSome() (obj interface{}, retErr error) {
114-
defer panik.ToError(&retErr) // de-escalate panic into error
115-
return []interface{} { "and then some", getEverything()... }, nil
113+
defer panik.ToError(&retErr) // de-escalate panic into error
114+
return append(getEverything(), "and then some"), nil
116115
}
117116

118117
func iAmAGoroutine(everythingChannel chan interface{}) interface{} {
119-
defer panik.RecoverTrace() // if the panic could not be handled, end it all with some logging
118+
defer panik.RecoverTraceTo(os.Stderr) // if the panic could not be handled, end it all with some logging
120119
everythingChannel<-getEverything()
121120
}
122121

123122
func getAnotherThing(id int) interface{} {
124123
if id == 42 {
125-
panik.Panicf("id %d is not supported", id) // panic from scratch when you have no non-nil error at hand
124+
panik.Panicf("cannot handle id %d", id) // panic from scratch when you have no non-nil error at hand
126125
}
127126
return things[id]
128127
}
128+
129+
func DoSomething() (retErr error) {
130+
defer panik.ToError(&retErr)
131+
panik.OnErrorfv(doSomethingInternal(), "could not do something") // eliminate type information about type "superSpecificError".
132+
133+
}
134+
135+
func doSomethingInternal() error {
136+
// ...
137+
return &superSpecificError{}
138+
}
129139
```
130140

131141
## Remarks
132142
* Use `panik.ToError()` at API boundaries. APIs which panic are not idiomatic Go.
143+
* Use `panik.ToErrorWithTrace()` if your error message is not informative enough by itself, but use it sparingly: stack traces are associated with programming errors; this will look bad if you don't know what the caller is going to do with the returned error.
133144
* Avoid calling `recover()` yourself. If you do, you take the responsibility of differing between panics caused by your code and panics of unknown origin using `panik.Caused()`. Also, [panik's trace-printing functions](#print-a-stack-trace) will have no visibility of the original panic. You basically lose all of the simplicity panik provides. It is adviced to use `panik.ToError()` instead.
134-
* You will still need to think about when to wrap an error and when to merely format its message; the types of wrapped errors are part of your API contract. See the difference between `OnErrorfw()` and `OnErrorfv()`.
145+
* You will still need to think about when to wrap an error and when to merely format its message; the types of wrapped errors are part of your API contract. See `OnErrorfv()` if you do not want to wrap an error.

panik.go

+32-44
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,10 @@ func Wrapf(format string, args ...interface{}) {
7878
panic(fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), makeCause(r)))
7979
}
8080

81-
// ToError recovers from any panic which originated from panik. This function
82-
// panics if errPtr is nil.
81+
// ToError recovers from any panic which originated from panik and writes the
82+
// recovered error to *errPtr.
83+
//
84+
// This function panics if errPtr is nil and does nothing if *errPtr is non-nil.
8385
func ToError(errPtr *error) {
8486
if errPtr == nil {
8587
panic("errPtr was nil")
@@ -97,28 +99,38 @@ func ToError(errPtr *error) {
9799
*errPtr = r.(error)
98100
}
99101

100-
// Caused returns true when r is or wraps an error which originated from panik.
101-
func Caused(r interface{}) bool {
102-
if err, isError := r.(error); isError {
103-
var known *knownCause
104-
return errors.As(err, &known)
102+
// ToErrorWithTrace recovers from any panic which originated from panik and writes
103+
// and error which wraps the recovered error to *errPtr and contains the stack trace
104+
// of the panic in its message.
105+
//
106+
// This function panics if errPtr is nil and does nothing if *errPtr is non-nil.
107+
func ToErrorWithTrace(errPtr *error) {
108+
if errPtr == nil {
109+
panic("errPtr was nil")
110+
}
111+
if *errPtr != nil {
112+
return
105113
}
106-
return false
107-
}
108-
109-
// RecoverTrace recovers from any panic and writes it to os.Stderr, the same way
110-
// that Go itself does when a goroutine terminates due to not having recovered
111-
// from a panic, but with excessive descends into panic.go and panik removed. If
112-
// there is no panic or the panic is nil, RecoverTrace does nothing.
113-
func RecoverTrace() {
114114
r := recover()
115115
if r == nil {
116116
return
117117
}
118+
if !Caused(r) {
119+
panic(r)
120+
}
118121
sb := bytes.NewBuffer(nil)
119122
tc := &traceCleaner{destination: sb}
120123
tc.Write(debug.Stack())
121-
os.Stderr.Write([]byte(fmt.Sprintf("recovered: %v:\n%s\n", r, string(sb.Bytes()))))
124+
*errPtr = fmt.Errorf("recovered: %w:\n%s", r, string(sb.Bytes()))
125+
}
126+
127+
// Caused returns true when r is or wraps an error which originated from panik.
128+
func Caused(r interface{}) bool {
129+
if err, isError := r.(error); isError {
130+
var known *knownCause
131+
return errors.As(err, &known)
132+
}
133+
return false
122134
}
123135

124136
// RecoverTraceTo recovers from any panic and writes it to the given writer, the
@@ -152,28 +164,7 @@ func RecoverTraceFunc(f func(trace string)) {
152164
f(fmt.Sprintf("recovered: %v:\n%s\n", r, string(sb.Bytes())))
153165
}
154166

155-
// ExitTrace recovers from any panic and writes it to os.Stderr, the same way
156-
// that Go itself does when a goroutine terminates due to not having recovered
157-
// from a panic, but with excessive descends into panic.go and panik removed,
158-
// and then calls os.Exit(2). If there is no panic or the panic is nil,
159-
// ExitTrace does nothing.
160-
func ExitTrace() {
161-
r := recover()
162-
if r == nil {
163-
return
164-
}
165-
sb := bytes.NewBuffer(nil)
166-
tc := &traceCleaner{destination: sb}
167-
tc.Write(debug.Stack())
168-
os.Stderr.Write([]byte(fmt.Sprintf("fatal: %v:\n%s\n", r, string(sb.Bytes()))))
169-
os.Exit(2)
170-
}
171-
172-
// ExitTraceTo recovers from any panic and writes it to the given writer, the
173-
// same way that Go itself does when a goroutine terminates due to not having
174-
// recovered from a panic, but with excessive descends into panic.go and panik
175-
// removed, and then calls os.Exit(2). If there is no panic or the panic is nil,
176-
// ExitTraceTo does nothing.
167+
// ExitTraceTo is like RecoverTraceTo, but also calls os.Exit(2) after writing to w.
177168
func ExitTraceTo(w io.Writer) {
178169
r := recover()
179170
if r == nil {
@@ -182,14 +173,11 @@ func ExitTraceTo(w io.Writer) {
182173
sb := bytes.NewBuffer(nil)
183174
tc := &traceCleaner{destination: sb}
184175
tc.Write(debug.Stack())
185-
w.Write([]byte(fmt.Sprintf("fatal: %v:\n%s\n", r, string(sb.Bytes()))))
176+
w.Write([]byte(fmt.Sprintf("panic: %v:\n%s\n", r, string(sb.Bytes()))))
186177
os.Exit(2)
187178
}
188179

189-
// ExitTraceFunc recovers from any panic and calls provided function with a stack trace,
190-
// formatted the same way that Go itself does when a goroutine terminates due to not having
191-
// recovered from a panic, but with excessive descends into panic.go and panik removed. If
192-
// there is no panic or the panic is nil, ExitTraceFunc does nothing.
180+
// ExitTraceFunc is like RecoverTraceFunc, but also calls os.Exit(2) after returning from f.
193181
func ExitTraceFunc(f func(trace string)) {
194182
r := recover()
195183
if r == nil {
@@ -198,6 +186,6 @@ func ExitTraceFunc(f func(trace string)) {
198186
sb := bytes.NewBuffer(nil)
199187
tc := &traceCleaner{destination: sb}
200188
tc.Write(debug.Stack())
201-
f(fmt.Sprintf("fatal: %v:\n%s\n", r, string(sb.Bytes())))
189+
f(fmt.Sprintf("panic: %v:\n%s\n", r, string(sb.Bytes())))
202190
os.Exit(2)
203191
}

trace_cleaner.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ var unwantedLineRegExps []*regexp.Regexp = []*regexp.Regexp{
4747
}
4848

4949
func isUnwantedLine(line string) bool {
50-
for _, verboseRegExp := range unwantedLineRegExps {
51-
if verboseRegExp.MatchString(line[:len(line)-1]) {
50+
for _, regExp := range unwantedLineRegExps {
51+
if regExp.MatchString(line[:len(line)-1]) {
5252
return true
5353
}
5454
}

trace_cleaner_test.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ func runTraceCleaner(t *testing.T, trace []byte, bytesPerCall int) []byte {
5454
write(t, traceCleaner, trace[i:limit])
5555
}
5656
}
57-
actualLineCount := len(strings.Split(string(buf.Bytes()), "\n"))
57+
cleanTrace := string(buf.Bytes())
58+
lines := strings.Split(cleanTrace, "\n")
59+
actualLineCount := len(lines)
5860
expectedLineCount := 8
5961

6062
// Expected:
@@ -70,8 +72,11 @@ func runTraceCleaner(t *testing.T, trace []byte, bytesPerCall int) []byte {
7072
t.Fatalf("Cleaned up trace has %d lines:\n%s\nExpected it to have %d lines. Original line count was %d.",
7173
actualLineCount, string(buf.Bytes()), expectedLineCount, originalLineCount)
7274
}
75+
if lines[7] != "" {
76+
t.Fatalf("Last line was not an empty line.")
77+
}
7378

74-
return buf.Bytes()
79+
return []byte(cleanTrace)
7580
}
7681

7782
func write(t *testing.T, w io.Writer, p []byte) {

0 commit comments

Comments
 (0)