lab.nexedi.com/kirr/go123@v0.0.0-20240207185015-8299741fa871/exc/error.go (about)

     1  // Copyright (C) 2015-2020  Nexedi SA and Contributors.
     2  //                          Kirill Smelkov <kirr@nexedi.com>
     3  //
     4  // This program is free software: you can Use, Study, Modify and Redistribute
     5  // it under the terms of the GNU General Public License version 3, or (at your
     6  // option) any later version, as published by the Free Software Foundation.
     7  //
     8  // You can also Link and Combine this program with other software covered by
     9  // the terms of any of the Free Software licenses or any of the Open Source
    10  // Initiative approved licenses and Convey the resulting work. Corresponding
    11  // source of such a combination shall include the source code for all other
    12  // software used.
    13  //
    14  // This program is distributed WITHOUT ANY WARRANTY; without even the implied
    15  // warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
    16  //
    17  // See COPYING file for full licensing terms.
    18  // See https://www.nexedi.com/licensing for rationale and options.
    19  
    20  // Package exc provides exception-style error handling for Go.
    21  //
    22  // Raise and Catch allow to raise and catch exceptions.
    23  //
    24  // By default the error caught is the same error that was raised. However with
    25  // Context* functions can arrange for context related to what they are doing to
    26  // be added to raised error as prefix, for example
    27  //
    28  //	func doSomething(path string) {
    29  //		defer exc.Context(func() interface{} {
    30  //			return fmt.Sprintf("doing something %s", path)
    31  //		})()
    32  //
    33  // or
    34  //
    35  //	func doSomething(path string) {
    36  //		defer exc.Contextf("doing something %s", path)
    37  //		...
    38  //
    39  // Lacking such Context annotations Addcallingcontext allows to add function
    40  // names up to the exception point as the calling context. However this way
    41  // only actions without corresponding arguments (path in the above example) can
    42  // be shown, and there have to be direct 1-1 relation between program and
    43  // operational structures.
    44  //
    45  // Runx allows to run a function which raises exception, and return exception
    46  // as regular error, if any. Similarly XRun allows to run a function which
    47  // returns regular error, and raise exception if error is not nil.
    48  //
    49  // Last but not least it has to be taken into account that exceptions
    50  // complicate control flow and are directly applicable only to serial programs.
    51  // Their use is thus justified in only limited number of cases and by default
    52  // one should always first strongly consider using explicit error returns
    53  // programming style which is canonical in Go.
    54  package exc
    55  
    56  import (
    57  	"fmt"
    58  	"runtime"
    59  	"strings"
    60  
    61  	"lab.nexedi.com/kirr/go123/my"
    62  	"lab.nexedi.com/kirr/go123/xruntime"
    63  )
    64  
    65  // Error is the type which is raised by Raise(arg).
    66  type Error struct {
    67  	arg  interface{}
    68  	link *Error // chain of linked Error(s) - see e.g. Context()
    69  }
    70  
    71  func (e *Error) Error() string {
    72  	msgv := []string{}
    73  	msg := ""
    74  	for e != nil {
    75  		if f, ok := e.arg.(runtime.Frame); ok {
    76  			//msg = f.Function
    77  			//msg = fmt.Sprintf("%s (%s:%d)", f.Function, f.File, f.Line)
    78  			msg = strings.TrimPrefix(f.Function, _errorpkgdot) // XXX -> better prettyfunc
    79  		} else {
    80  			msg = fmt.Sprint(e.arg)
    81  		}
    82  		msgv = append(msgv, msg)
    83  		e = e.link
    84  	}
    85  
    86  	return strings.Join(msgv, ": ")
    87  }
    88  
    89  // Aserror turns any value into Error.
    90  //
    91  // if v is already Error - it stays the same,
    92  // otherwise new Error is created.
    93  func Aserror(v interface{}) *Error {
    94  	if e, ok := v.(*Error); ok {
    95  		return e
    96  	}
    97  	return &Error{v, nil}
    98  }
    99  
   100  // Raise raise error to upper level.
   101  //
   102  // See Catch which receives raised error.
   103  func Raise(arg interface{}) {
   104  	panic(Aserror(arg))
   105  }
   106  
   107  // Raisef raises formatted string.
   108  func Raisef(format string, a ...interface{}) {
   109  	panic(Aserror(fmt.Sprintf(format, a...)))
   110  }
   111  
   112  // Raiseif raises if err != nil.
   113  //
   114  // NOTE err can be != nil even if typed obj = nil:
   115  //
   116  //	var obj *T;
   117  //	err = obj
   118  //	err != nil     is true
   119  func Raiseif(err error) {
   120  	//if err != nil && !reflect.ValueOf(err).IsNil() {
   121  	if err != nil {
   122  		panic(Aserror(err))
   123  	}
   124  }
   125  
   126  // _errcatch checks recovered value to be of type *Error.
   127  //
   128  // if there is non-Error error - repanic it,
   129  // otherwise return Error either nil (no panic), or actual value.
   130  func _errcatch(r interface{}) *Error {
   131  	e, _ := r.(*Error)
   132  	if e == nil && r != nil {
   133  		panic(r)
   134  	}
   135  	return e
   136  }
   137  
   138  // Catch catches error and calls f(e) if it was caught.
   139  //
   140  // Must be called under defer.
   141  func Catch(f func(e *Error)) {
   142  	e := _errcatch(recover())
   143  	if e == nil {
   144  		return
   145  	}
   146  
   147  	f(e)
   148  }
   149  
   150  // Onunwind installs error filter to be applied on error unwinding.
   151  //
   152  // It hooks into unwinding process with f() call. Returned error is reraised.
   153  // see also: Context()
   154  //
   155  // Must be called under defer.
   156  func Onunwind(f func(e *Error) *Error) {
   157  	// cannot do Catch(...)
   158  	// as recover() works only in first-level called functions
   159  	e := _errcatch(recover())
   160  	if e == nil {
   161  		return
   162  	}
   163  
   164  	e = f(e)
   165  	panic(e)
   166  }
   167  
   168  // Context provides error context to be added on unwinding.
   169  //
   170  // f is called if error unwinding is happening and its
   171  // result is added to raised error as "prefix" context.
   172  //
   173  // Must be called under defer.
   174  func Context(f func() interface{}) {
   175  	e := _errcatch(recover())
   176  	if e == nil {
   177  		return
   178  	}
   179  
   180  	arg := f()
   181  	panic(Addcontext(e, arg))
   182  }
   183  
   184  // Contextf provides formatted string to be added to error as context on unwinding.
   185  //
   186  // It is shorthand for Context wrapping fmt.Sprintf.
   187  //
   188  // Must be called under defer.
   189  func Contextf(format string, argv ...interface{}) {
   190  	e := _errcatch(recover())
   191  	if e == nil {
   192  		return
   193  	}
   194  
   195  	panic(Addcontext(e, fmt.Sprintf(format, argv...)))
   196  }
   197  
   198  // Addcontext adds "prefix" context to error.
   199  func Addcontext(e *Error, arg interface{}) *Error {
   200  	return &Error{arg, e}
   201  }
   202  
   203  var (
   204  	_errorpkgname string // package name under which error.go lives
   205  	_errorpkgdot  string // errorpkg.
   206  	_errorraise   string // errorpkg.Raise
   207  )
   208  
   209  func init() {
   210  	_errorpkgname	= my.PkgName()
   211  	_errorpkgdot	= _errorpkgname + "."
   212  	_errorraise	= _errorpkgname + ".Raise"
   213  }
   214  
   215  // Addcallingcontext adds calling context to error.
   216  //
   217  // Add calling function frames as error context up-to topfunc not including.
   218  //
   219  // See also: Addcontext()
   220  func Addcallingcontext(topfunc string, e *Error) *Error {
   221  	seenraise := false
   222  	for _, f := range xruntime.Traceback(2) {
   223  		// do not show anything after raise*()
   224  		if !seenraise && strings.HasPrefix(f.Function, _errorraise) {
   225  			seenraise = true
   226  			continue
   227  		}
   228  		if !seenraise {
   229  			continue
   230  		}
   231  
   232  		// do not go beyond topfunc
   233  		if topfunc != "" && f.Function == topfunc {
   234  			break
   235  		}
   236  
   237  		// skip intermediates
   238  		if strings.HasSuffix(f.Function, "_") { // XXX -> better skipfunc
   239  			continue
   240  		}
   241  
   242  		e = &Error{f, e}
   243  	}
   244  
   245  	return e
   246  }
   247  
   248  // Runx runs a function which raises exception, and return exception as regular error, if any.
   249  //
   250  // the error, if non-nil, will be returned with added calling context - see
   251  // Addcallingcontext for details.
   252  //
   253  // See also: Funcx.
   254  func Runx(xf func()) (err error) {
   255  	return Funcx(xf)()
   256  }
   257  
   258  // Funcx converts a function raising exception, to function returning regular error.
   259  //
   260  // Returned function calls xf and converts exception, if any, to error.
   261  //
   262  // See also: Runx.
   263  func Funcx(xf func()) func() error {
   264  	return func() (err error) {
   265  		here := my.FuncName()
   266  		defer Catch(func(e *Error) {
   267  			err = Addcallingcontext(here, e)
   268  		})
   269  
   270  		xf()
   271  		return
   272  	}
   273  }
   274  
   275  // XRun runs a function which returns regular error, and raise exception if error is not nil.
   276  //
   277  // See also: XFunc.
   278  func XRun(f func() error) {
   279  	XFunc(f)()
   280  }
   281  
   282  // XFunc converts a function returning regular error, to function raising exception.
   283  //
   284  // Returned function calls f and raises appropriate exception if error is not nil.
   285  //
   286  // See also: XRun.
   287  func XFunc(f func() error) func() {
   288  	return func() {
   289  		err := f()
   290  		Raiseif(err)
   291  	}
   292  }