GithubHelp home page GithubHelp logo

errors's Introduction

errors

import "github.com/juju/errors"

GoDoc

The juju/errors provides an easy way to annotate errors without losing the original error context.

The exported New and Errorf functions are designed to replace the errors.New and fmt.Errorf functions respectively. The same underlying error is there, but the package also records the location at which the error was created.

A primary use case for this library is to add extra context any time an error is returned from a function.

    if err := SomeFunc(); err != nil {
	    return err
	}

This instead becomes:

    if err := SomeFunc(); err != nil {
	    return errors.Trace(err)
	}

which just records the file and line number of the Trace call, or

    if err := SomeFunc(); err != nil {
	    return errors.Annotate(err, "more context")
	}

which also adds an annotation to the error.

When you want to check to see if an error is of a particular type, a helper function is normally exported by the package that returned the error, like the os package does. The underlying cause of the error is available using the Cause function.

os.IsNotExist(errors.Cause(err))

The result of the Error() call on an annotated error is the annotations joined with colons, then the result of the Error() method for the underlying error that was the cause.

err := errors.Errorf("original")
err = errors.Annotatef(err, "context")
err = errors.Annotatef(err, "more context")
err.Error() -> "more context: context: original"

Obviously recording the file, line and functions is not very useful if you cannot get them back out again.

errors.ErrorStack(err)

will return something like:

first error
github.com/juju/errors/annotation_test.go:193:
github.com/juju/errors/annotation_test.go:194: annotation
github.com/juju/errors/annotation_test.go:195:
github.com/juju/errors/annotation_test.go:196: more context
github.com/juju/errors/annotation_test.go:197:

The first error was generated by an external system, so there was no location associated. The second, fourth, and last lines were generated with Trace calls, and the other two through Annotate.

Sometimes when responding to an error you want to return a more specific error for the situation.

    if err := FindField(field); err != nil {
	    return errors.Wrap(err, errors.NotFoundf(field))
	}

This returns an error where the complete error stack is still available, and errors.Cause() will return the NotFound error.

func AlreadyExistsf

func AlreadyExistsf(format string, args ...interface{}) error

AlreadyExistsf returns an error which satisfies IsAlreadyExists().

func Annotate

func Annotate(other error, message string) error

Annotate is used to add extra context to an existing error. The location of the Annotate call is recorded with the annotations. The file, line and function are also recorded.

For example:

if err := SomeFunc(); err != nil {
    return errors.Annotate(err, "failed to frombulate")
}

func Annotatef

func Annotatef(other error, format string, args ...interface{}) error

Annotatef is used to add extra context to an existing error. The location of the Annotate call is recorded with the annotations. The file, line and function are also recorded.

For example:

if err := SomeFunc(); err != nil {
    return errors.Annotatef(err, "failed to frombulate the %s", arg)
}

func BadRequestf

func BadRequestf(format string, args ...interface{}) error

BadRequestf returns an error which satisfies IsBadRequest().

func Cause

func Cause(err error) error

Cause returns the cause of the given error. This will be either the original error, or the result of a Wrap or Mask call.

Cause is the usual way to diagnose errors that may have been wrapped by the other errors functions.

func DeferredAnnotatef

func DeferredAnnotatef(err *error, format string, args ...interface{})

DeferredAnnotatef annotates the given error (when it is not nil) with the given format string and arguments (like fmt.Sprintf). If *err is nil, DeferredAnnotatef does nothing. This method is used in a defer statement in order to annotate any resulting error with the same message.

For example:

defer DeferredAnnotatef(&err, "failed to frombulate the %s", arg)

func Details

func Details(err error) string

Details returns information about the stack of errors wrapped by err, in the format:

[{filename:99: error one} {otherfile:55: cause of error one}]

This is a terse alternative to ErrorStack as it returns a single line.

func ErrorStack

func ErrorStack(err error) string

ErrorStack returns a string representation of the annotated error. If the error passed as the parameter is not an annotated error, the result is simply the result of the Error() method on that error.

If the error is an annotated error, a multi-line string is returned where each line represents one entry in the annotation stack. The full filename from the call stack is used in the output.

first error
github.com/juju/errors/annotation_test.go:193:
github.com/juju/errors/annotation_test.go:194: annotation
github.com/juju/errors/annotation_test.go:195:
github.com/juju/errors/annotation_test.go:196: more context
github.com/juju/errors/annotation_test.go:197:

func Errorf

func Errorf(format string, args ...interface{}) error

Errorf creates a new annotated error and records the location that the error is created. This should be a drop in replacement for fmt.Errorf.

For example:

return errors.Errorf("validation failed: %s", message)

func Forbiddenf

func Forbiddenf(format string, args ...interface{}) error

Forbiddenf returns an error which satistifes IsForbidden()

func IsAlreadyExists

func IsAlreadyExists(err error) bool

IsAlreadyExists reports whether the error was created with AlreadyExistsf() or NewAlreadyExists().

func IsBadRequest

func IsBadRequest(err error) bool

IsBadRequest reports whether err was created with BadRequestf() or NewBadRequest().

func IsForbidden

func IsForbidden(err error) bool

IsForbidden reports whether err was created with Forbiddenf() or NewForbidden().

func IsMethodNotAllowed

func IsMethodNotAllowed(err error) bool

IsMethodNotAllowed reports whether err was created with MethodNotAllowedf() or NewMethodNotAllowed().

func IsNotAssigned

func IsNotAssigned(err error) bool

IsNotAssigned reports whether err was created with NotAssignedf() or NewNotAssigned().

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports whether err was created with NotFoundf() or NewNotFound().

func IsNotImplemented

func IsNotImplemented(err error) bool

IsNotImplemented reports whether err was created with NotImplementedf() or NewNotImplemented().

func IsNotProvisioned

func IsNotProvisioned(err error) bool

IsNotProvisioned reports whether err was created with NotProvisionedf() or NewNotProvisioned().

func IsNotSupported

func IsNotSupported(err error) bool

IsNotSupported reports whether the error was created with NotSupportedf() or NewNotSupported().

func IsNotValid

func IsNotValid(err error) bool

IsNotValid reports whether the error was created with NotValidf() or NewNotValid().

func IsUnauthorized

func IsUnauthorized(err error) bool

IsUnauthorized reports whether err was created with Unauthorizedf() or NewUnauthorized().

func IsUserNotFound

func IsUserNotFound(err error) bool

IsUserNotFound reports whether err was created with UserNotFoundf() or NewUserNotFound().

func Mask

func Mask(other error) error

Mask hides the underlying error type, and records the location of the masking.

func Maskf

func Maskf(other error, format string, args ...interface{}) error

Mask masks the given error with the given format string and arguments (like fmt.Sprintf), returning a new error that maintains the error stack, but hides the underlying error type. The error string still contains the full annotations. If you want to hide the annotations, call Wrap.

func MethodNotAllowedf

func MethodNotAllowedf(format string, args ...interface{}) error

MethodNotAllowedf returns an error which satisfies IsMethodNotAllowed().

func New

func New(message string) error

New is a drop in replacement for the standard library errors module that records the location that the error is created.

For example:

return errors.New("validation failed")

func NewAlreadyExists

func NewAlreadyExists(err error, msg string) error

NewAlreadyExists returns an error which wraps err and satisfies IsAlreadyExists().

func NewBadRequest

func NewBadRequest(err error, msg string) error

NewBadRequest returns an error which wraps err that satisfies IsBadRequest().

func NewForbidden

func NewForbidden(err error, msg string) error

NewForbidden returns an error which wraps err that satisfies IsForbidden().

func NewMethodNotAllowed

func NewMethodNotAllowed(err error, msg string) error

NewMethodNotAllowed returns an error which wraps err that satisfies IsMethodNotAllowed().

func NewNotAssigned

func NewNotAssigned(err error, msg string) error

NewNotAssigned returns an error which wraps err that satisfies IsNotAssigned().

func NewNotFound

func NewNotFound(err error, msg string) error

NewNotFound returns an error which wraps err that satisfies IsNotFound().

func NewNotImplemented

func NewNotImplemented(err error, msg string) error

NewNotImplemented returns an error which wraps err and satisfies IsNotImplemented().

func NewNotProvisioned

func NewNotProvisioned(err error, msg string) error

NewNotProvisioned returns an error which wraps err that satisfies IsNotProvisioned().

func NewNotSupported

func NewNotSupported(err error, msg string) error

NewNotSupported returns an error which wraps err and satisfies IsNotSupported().

func NewNotValid

func NewNotValid(err error, msg string) error

NewNotValid returns an error which wraps err and satisfies IsNotValid().

func NewUnauthorized

func NewUnauthorized(err error, msg string) error

NewUnauthorized returns an error which wraps err and satisfies IsUnauthorized().

func NewUserNotFound

func NewUserNotFound(err error, msg string) error

NewUserNotFound returns an error which wraps err and satisfies IsUserNotFound().

func NotAssignedf

func NotAssignedf(format string, args ...interface{}) error

NotAssignedf returns an error which satisfies IsNotAssigned().

func NotFoundf

func NotFoundf(format string, args ...interface{}) error

NotFoundf returns an error which satisfies IsNotFound().

func NotImplementedf

func NotImplementedf(format string, args ...interface{}) error

NotImplementedf returns an error which satisfies IsNotImplemented().

func NotProvisionedf

func NotProvisionedf(format string, args ...interface{}) error

NotProvisionedf returns an error which satisfies IsNotProvisioned().

func NotSupportedf

func NotSupportedf(format string, args ...interface{}) error

NotSupportedf returns an error which satisfies IsNotSupported().

func NotValidf

func NotValidf(format string, args ...interface{}) error

NotValidf returns an error which satisfies IsNotValid().

func Trace

func Trace(other error) error

Trace adds the location of the Trace call to the stack. The Cause of the resulting error is the same as the error parameter. If the other error is nil, the result will be nil.

For example:

if err := SomeFunc(); err != nil {
    return errors.Trace(err)
}

func Unauthorizedf

func Unauthorizedf(format string, args ...interface{}) error

Unauthorizedf returns an error which satisfies IsUnauthorized().

func UserNotFoundf

func UserNotFoundf(format string, args ...interface{}) error

UserNotFoundf returns an error which satisfies IsUserNotFound().

func Wrap

func Wrap(other, newDescriptive error) error

Wrap changes the Cause of the error. The location of the Wrap call is also stored in the error stack.

For example:

if err := SomeFunc(); err != nil {
    newErr := &packageError{"more context", private_value}
    return errors.Wrap(err, newErr)
}

func Wrapf

func Wrapf(other, newDescriptive error, format string, args ...interface{}) error

Wrapf changes the Cause of the error, and adds an annotation. The location of the Wrap call is also stored in the error stack.

For example:

if err := SomeFunc(); err != nil {
    return errors.Wrapf(err, simpleErrorType, "invalid value %q", value)
}

type Err

type Err struct {
    // contains filtered or unexported fields
}

Err holds a description of an error along with information about where the error was created.

It may be embedded in custom error types to add extra information that this errors package can understand.

func NewErr

func NewErr(format string, args ...interface{}) Err

NewErr is used to return an Err for the purpose of embedding in other structures. The location is not specified, and needs to be set with a call to SetLocation.

For example:

type FooError struct {
    errors.Err
    code int
}

func NewFooError(code int) error {
    err := &FooError{errors.NewErr("foo"), code}
    err.SetLocation(1)
    return err
}

func NewErrWithCause

func NewErrWithCause(other error, format string, args ...interface{}) Err

NewErrWithCause is used to return an Err with cause by other error for the purpose of embedding in other structures. The location is not specified, and needs to be set with a call to SetLocation.

For example:

type FooError struct {
    errors.Err
    code int
}

func (e *FooError) Annotate(format string, args ...interface{}) error {
    err := &FooError{errors.NewErrWithCause(e.Err, format, args...), e.code}
    err.SetLocation(1)
    return err
})

func (*Err) Cause

func (e *Err) Cause() error

The Cause of an error is the most recent error in the error stack that meets one of these criteria: the original error that was raised; the new error that was passed into the Wrap function; the most recently masked error; or nil if the error itself is considered the Cause. Normally this method is not invoked directly, but instead through the Cause stand alone function.

func (*Err) Error

func (e *Err) Error() string

Error implements error.Error.

func (*Err) Format

func (e *Err) Format(s fmt.State, verb rune)

Format implements fmt.Formatter When printing errors with %+v it also prints the stack trace. %#v unsurprisingly will print the real underlying type.

func (*Err) Location

func (e *Err) Location() (filename string, line int)

Location is the file and line of where the error was most recently created or annotated.

func (*Err) Message

func (e *Err) Message() string

Message returns the message stored with the most recent location. This is the empty string if the most recent call was Trace, or the message stored with Annotate or Mask.

func (*Err) SetLocation

func (e *Err) SetLocation(callDepth int)

SetLocation records the source location of the error at callDepth stack frames above the call.

func (*Err) StackTrace

func (e *Err) StackTrace() []string

StackTrace returns one string for each location recorded in the stack of errors. The first value is the originating error, with a line for each other annotation or tracing of the error.

func (*Err) Underlying

func (e *Err) Underlying() error

Underlying returns the previous error in the error stack, if any. A client should not ever really call this method. It is used to build the error stack and should not be introspected by client calls. Or more specifically, clients should not depend on anything but the Cause of an error.


Generated by godoc2md

errors's People

Contributors

achilleasa avatar axw avatar aznashwan avatar barrettj12 avatar bogdanteleaga avatar davecheney avatar ericsnowcurrently avatar fwereade avatar geoffreybauduin avatar gertcuykens avatar hiveminded avatar howbazaar avatar hpidcock avatar jameinel avatar jason-cooke avatar jmoiron avatar johnweldon avatar jujubot avatar lysu avatar mattyw avatar rogpeppe avatar spring1843 avatar temoto avatar tlm avatar tt avatar wallyworld avatar ycliuhw avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

errors's Issues

Juju Errors V2 Proposal ๐ŸŽ‰

Introduction

Since juju/errors was first concieved a lot of changes have happened in the Go errors space that are starting to be in conflict with the design of this library. We have a chance to cut a new v2 implementation of this library building on top of what is on offer in std errors.

This issue aims to both document my thoughts around what this new library might look like and solicit further design feedback and consensus before proceeding. Being a v2 of this library there will be a number of breaking changes, the migration plan for how we move Juju to this new library if it is adopted is also documented below.

This work should be considered out of band to the current concerns we have going on for Juju and it is my plan to offer up the changes to the library while traveling to get the work done and dusted.

New Design

The new design being proposed below is taking an opinionated design on how to deal with errors that occur during program execution. Some of these opinionated elements may end up being left out of this change based on feed back recieved.

Removal Of Utility Error Types

The first opionated change being proposed is the removal of the const error types defined in v1 of this library such as NotFound & Timeout. While these error types are useful they are far to broad in the problem they are describing and don't clearly indicate to the users of the error which specific part has failed or a reliable way to assert exact problems.

If you recieve a NotFound error what exactly wasn't found and at which level in the error chain is the NotFound error located at? As apposed to package level errors that are communicating specific NotFound instances. For example:

func PackageFunc() error {
  if err := operation1(); err != nil {
    return err
  }
  
  if err := operation2(); err != nil {
    return err
  }
}

If both operation1 and operation2 return errors that satisfy errors.NotFound then how do we know specifically what wasn't found? As opposed to:

func operation1() error { return ErrorThingXNotFound }
func operation2() error { return ErrorThingYNotFound }

Now with package specific errors the caller can both assert exactly what wasn't found and even produce better error message for the end user on how to rectify the situation.

What I would like to see is the error types such as NotFound and Timeout moved into a new package in Juju as core/errors and have their respective usages decline over time. Moving them to Juju allows us to both keep using the error types where they make sense and provide better package level errors. This idea is discussed a little more further down.

Removal Of High Level Tracing Funcs

A common pattern we have in Juju is tracing returned errors up a call stack till a caller wishes to process the error and deal with it. Example of how this works:

func One() error {
  return errors.Trace(Two())
}

func Two() error {
  return errors.Trace(Three())
}

func Three() error {
  return errors.New("some error")
}

err := One()
fmt.Println(err)
// some error
fmt.Println(ErrorStack(err))
// github.com/juju/juju/mypackage/test.go:10: some error
// github.com/juju/juju/mypackage/test.go:15: some error
// github.com/juju/juju/mypackage/test.go:20: some error

The ability to trace errors allows for a debuging perspective later on to take an error and figure out where it has originated from. Adding Trace() at each level annotates the error with the last known place something touched the error.

The problem that I see with this approach is it's very coarse and seems to be hiding two problems that we face when dealing with errors. The first is that errors generated don't contain enough information and context for what has gone wrong and the second is we aren't annotating errors well as they traverse a call stack in Juju.

With this tracing we might know where an error originated from but we don't have enough context for what was the precondition into the error. With traced errors we also have to have the forsight to dump the stack for the error with ErrorStack as they aren't naturally surfaced.

The new design of this library explicitly takes away the high level Trace() function from the user until an operation is performed that first enriches the error with more context. This is a deliberate design decision to promote better error annotating first before tracing. As an example:

func One() error {
  return errors.Errorf("executing api request for user %q to update model metadata: %w", "bob", Two()).Trace()
}

func Two() error {
  return errors.Errorf("performing model metadata update: %w", Three()).Trace()
}

func Three() error {
  return errors.Errorf("model %q constraint failed precondition %q", uuid, precondition).Trace()
}

err := One()
fmt.Println(One())
// executing api request for user "bob" to update model metadata: performing model metadata update: model "123" constraint failed precondition "some-key"
fmt.Println(ErrorStack(err))
// github.com/juju/juju/mypackage/test.go:10: executing api request for user "bob" to update model metadata: performing model metadata update: model "123" constraint failed precondition "some-key"
// github.com/juju/juju/mypackage/test.go:15: performing model metadata update: model "123" constraint failed precondition "some-key"
// github.com/juju/juju/mypackage/test.go:20: model "123" constraint failed precondition "some-key"

From the example above you can see that tracing is not performed automatically like it is for most calls to the current errors library. It would be an explicit decision by the user. Forcing the error to first be enriched makes us as developers add more context to the errors first removing one of the main use cases for Trace().

The new ideas demonstrated above are discussed more below. The new approach is not perfect in every aspect and will produce much more verbose error strings.

Core Idea

With this new design I would like to have it focus around two central types.

Const Error

This is the same new type introduced in v1 with no additional changes. The following properties will still remain true about the error.

type ConstError string

const ErrorDoohickey = ConstError("doohickey failure")

const ErrorOther = ConstError("doohickey failure")

const ErrorNoMatch = ConstError("no match for doohickey")

fmt.Println(ErrorDoohickey == ErrOther)
// True

fmt.Println(ErrorNoMatch == ErrOther)
// False

newErr := fmt.Errorf("foo bar %w", ErrorDoohickey)
fmt.Println(newErr)
// foo bar doohickey failure

fmt.Println(errors.Is(newErr, ErrDoohickey))
// True

fmt.Println(errors.Is(newErr, ErrNoMatch))
// False

Error

A new type introduced into this library and not to be confused with any existing Error types that exist in v1. The idea of this type is to introduce a builder pattern where by once you have a variable of this type you can further build on the error.

Errors are to be formed in chains like err1 -> err2 -> err3 where each new error in the chain is offering a new piece of information to help the end user make better decisions.

The following type Error is proposed.

// Error represents a Go error generated by this library that can further be built on. All Errors are immutable.
type Error interface {
  // error represents the Go error encapsulated by this type and makes clear that
  // Error conforms to the error interface as well.
  error
  
  // Add adds the error to this error returning an error chain that satisfies the
  // added error. The newly constructed chain will be origError -> addedError. The
  // new Error returned by add does not change the original error.Error() message.
  // This is useful in cases where the producer of the error would like the error to
  // satisfy some existing error type but not have the added errors Error() string
  // pollute the message of the original error.
  // In v1 of this library this functionality was similar to errors.Hide()
  //
  // Example:
  // const ErrorInvalid = ConstError("invalid operation")
  //
  // e := errors.Errorf("user id %q is not understood", userId).Add(ErrorInvalid)
  //
  // fmt.Println(e.Error())
  // // user id "bluey" is not understood
  //
  // fmt.Println(errors.Is(e, ErrorInvalid))
  // // True
  Add(error) Error
  
  // Trace returns a new error indicating the source code file and line number where
  // the Error.Trace() function was called from.
  // See ErrorStack for usage.
  Trace() Error
  
  // Unwrap implements stderrors.Unwrap().
  Unwrap() error
}

Deliberate design has been made in designing Error so that it cannot be directly constructed by the end user and can only be obtained by using the global functions of this library. At the moment this new design is pushing the idea that errors should be handled an enriched instead of just annotated with debugging information and passed up the stack blindly.

Static Functions

This section documents the Global static functions on the package and their purpose.

std errors

The following new functions will be added to the library to maintain comptability with std errors.

Join adds the currently missing std errors.Join() function, altering it to now return the Error type offered by this package so the error can be further enriched.

// Join returns an error that wraps the given errors. Any nil error values are
// discarded. Join returns nil if every value in errs is nil. The error formats as
// the concatenation of the strings obtained by calling the Error method of each
// element of errs, with a newline between each string.
//
// A non-nil error returned by Join implements the Unwrap() []error method.
func Join(errs ...error) Error

The remaining functions already exist in this package with proposed changes indicated.

// As is a 1:1 implementation of errors.As
func As(error, any) bool

// Is is a 1:1 implementation of errors.Is
func Is(error, error) bool

// New is a 1:1 implementation of errors.New with the difference being that a
// Error is returned as the resulting type so that the error can further be
// enriched.
//
// Changes from v1: New will no longer return either a caused based error or
// a error that has been traced. If the caller wishes to trace the error they
// can perform New("my error").Trace()
func New(string) Error

// Unwrap is a 1:1 implementation of errors.Unwrap
func Unwrap(error) error

std errors Helpers

In v1 of this library we introduced the following std errors helpers to extend the functionality. This library will retain their functionality.

AsType

// AsType finds the first error in err's chain that is assignable to type T, and if a match is found, returns that error value and true. Otherwise, it returns T's zero value and false.
// AsType is equivalent to errors.As, but uses a type parameter and returns the target, to avoid having to define a variable before the call.
func AsType[T error](err error) (T, bool)

HasType

// HasType is a function wrapper around AsType dropping the where return value from AsType().
func HasType[T error](err error) bool

fmt errors

The following fmt functions will be included in the set of static functions offered by this package.

// Errorf is a 1:1 implementation of fmt.Errorf with the difference being that
// Error is returned as the resulting type so that the error can further be
// enriched.
//
// Changes from v1: Errorf will no longer return either a caused based error
// or a error that has been traced. If the caller wishes to trace the error they
// can perform Errorf("id %q not found", id).Trace()
func Errorf(string, ...any) Error

Tracing errors

To maintain tracing support in the library we will maintain the following functions.

// ErrorStack returns a string with a new line for each error in the chain. If the error contains tracing information that will also be printed with the error.
func ErrorStack(error) string

Removals

The following sections list the types and functions I would like to see removed from this package. With each removal a justification has been provided.

Annotation Funcs

The following annotation functions should be removed with justification.

// Wrap* based functions are confusing in their signature and offer no real
// added benefit. They can be directly replaced with calls to Errorf() using
// %w masks for the format string or combined using Join(). By not using Wrap
// we avoid the potential pitfuls of Caused based errors currently introduced
// Wrap.
func Wrap(other, newDescriptive error) error
func Wrapf(other, newDescriptive error, format string, args ...interface{}) error
// WithType was introduced as part of the support for the new std errors
// package in go. It offered a way to maintain an already established error
// but now make it also satisfy Is for a ConstError type without introducing
// a tree structure of error chains.
//
// We don't have this limitation anymore and can safely use the new Errors.Add()
// method for adding new error types into the chain.
func WithType(error, ConstError) error
// Maskf is used to produce a new error with the format string and args
// hiding the original error from both the error.Error() string and the
// error type. This resultant error needs Cause() called on it to unpack
// the hidden error.
//
// We have 2 uses of this func inside of Juju and they can be replaced with
// the same effect using Errorf("", args...).Add(otherError)
func Maskf(error, string, ...any) error
// Hide was introduced as part of the upgrade to std errors. It's purpose
// was to hide printing of an errors Error message in calls to fmt.Errorf
// but still have the error available in the error chain so calls to Is
// and As still worked.
//
// These calls can be replaced with Errors.Add() for the same effect now.
func Hide(error) error

IsError funcs

All of the Is*() error of this type functions. They have been deprecated for a while now and we have now moved off using them in the latest versions of Juju. All of these calls should now be aligned on to the go std errors pkg error.Is() idiom.

func IsAlreadyExists(err error) bool {...}
func IsBadRequest(err error) bool {}
func IsForbidden(err error) bool
func IsMethodNotAllowed(err error) bool
func IsNotAssigned(err error) bool
func IsNotFound(err error) bool
func IsNotImplemented(err error) bool
func IsNotProvisioned(err error) bool
func IsNotSupported(err error) bool
func IsNotValid(err error) bool
func IsNotYetAvailable(err error) bool
func IsQuotaLimitExceeded(err error) bool
func IsTimeout(err error) bool
func IsUnauthorized(err error) bool
func IsUserNotFound(err error) bool

Error Type Codes

Currently this library has a pre-existing list of common error types that we offer to the user to communicate common error scenarios. For example errors.NotFound, largely these are both based off of common HTTP status codes and Juju's own requirements.

From my point of view I think they offer the following problems:

  • They're specific to a single domain, being that of Juju
  • They are far too wide in the scope they describe. For example if you returned a NotFound error from a function what specifically wasn't found? That is to say we may want to still check if an error is a type of NotFound error but I think we should be actively moving away from describing problems with such high level types and opting for usage of package level errors.

What I would like to propose is the following:

Deprecate The Following Types in V1

This would see the following types and functions deprecated in v1 of this library.

const (
	Timeout = ConstError("timeout")
	NotFound = ConstError("not found")
	UserNotFound = ConstError("user not found")
	Unauthorized = ConstError("unauthorized")
	NotImplemented = ConstError("not implemented")
	AlreadyExists = ConstError("already exists")
	NotSupported = ConstError("not supported")
	NotValid = ConstError("not valid")
	NotProvisioned = ConstError("not provisioned")
	NotAssigned = ConstError("not assigned")
	BadRequest = ConstError("bad request")
	MethodNotAllowed = ConstError("method not allowed")
	Forbidden = ConstError("forbidden")
	QuotaLimitExceeded = ConstError("quota limit exceeded")
	NotYetAvailable = ConstError("not yet available")
)

func AlreadyExistsf(format string, args ...interface{}) error
func BadRequestf(format string, args ...interface{}) error
func Forbiddenf(format string, args ...interface{}) error
func MethodNotAllowedf(format string, args ...interface{}) error
func NewAlreadyExists(err error, msg string) error
func NewBadRequest(err error, msg string) error
func NewForbidden(err error, msg string) error
func NewMethodNotAllowed(err error, msg string) error
func NewNotAssigned(err error, msg string) error
func NewNotFound(err error, msg string) error
func NewNotImplemented(err error, msg string) error
func NewNotProvisioned(err error, msg string) error
func NewNotSupported(err error, msg string) error
func NewNotValid(err error, msg string) error
func NewNotYetAvailable(err error, msg string) error
func NewQuotaLimitExceeded(err error, msg string) error
func NewTimeout(err error, msg string) error
func NewUnauthorized(err error, msg string) error
func NewUserNotFound(err error, msg string) error
func NotAssignedf(format string, args ...interface{}) error
func NotFoundf(format string, args ...interface{}) error
func NotImplementedf(format string, args ...interface{}) error
func NotProvisionedf(format string, args ...interface{}) error
func NotSupportedf(format string, args ...interface{}) error
func NotValidf(format string, args ...interface{}) error
func NotYetAvailablef(format string, args ...interface{}) error
func QuotaLimitExceededf(format string, args ...interface{}) error
func Timeoutf(format string, args ...interface{}) error
func Trace(other error) error
func Unauthorizedf(format string, args ...interface{}) error
func Unwrap(err error) error
func UserNotFoundf(format string, args ...interface{}) error

Reimplement The Deprecated Types

With the types above being deprecated we would move them into the Juju code based under core/errors. To aid in the transition these types can be set to their deprecated counter parts from the juju/errors package.

const (
  Timeout = errors.Timeout
  ...
)

var (
  AlreadyExistsf = errors.AlreadyExistsf
  NewAlreadyExists = errors.NewAlreadyExists
  ...
)

Transition

With breaking changes being introduced we need a way to transition to the v2 errors module should this proposal be adopted. My current plan for doing this is as follows:

  • Introduce errors/v2 changes in juju main only. Our older branches for released versions can stay on errors/v1. We will have to take on some associated merge forward overhead if the juju main branch ever fully cuts off of errors/v1.
  • Have both github.com/juju/errors and github.com/juju/errors/v2 modules imports in our go.mod file. All new code written should be written against v2 and when modifying the errors of an existing file the author should take the time to move that file over to the new package where it makes sense.
  • We would need to transition all of our libraries that rely on juju/errors over to v2. The main hassle here based on the proposal is library usage of ConstErrors defined in v1 such as NotFound or Timeout. Ideally we would force our libraries to make public const errors that describe their own errors and move Juju over to using domain specific errors from the respective library. As an example this would be a common case we have today:
    // Library code
    func MyLibraryFuncCheckThingX() error {
       // ...
      return errors.NotFound
    }
    
    // Juju Code
    err := MyLibraryFuncCheckThingX()
    fmt.Println(errors.Is(err, errors.NotFound))
    // True
    The first way we can aid this transition is to make our libaries return errors that satisfy both the original error value and the new const errors defined by the library. This would allow us to not have major version bumps in our libraries for the change. Example:
    const ErrorThingNotFound = errors.ConstError("thing not found")
    func MyLibraryFuncCheckThingX() error {
      // ...
      return errors.Errorf("%w some id", ErrorThingNotFound).Add(errorsv1.NotFound) 
    }
    
    // Juju Code
    err := MyLibraryFuncCheckThingX()
    fmt.Println(errors.Is(err, errors.NotFound))
    // True
    fmt.Println(errors.Is(err, mylib.ErrorThingNotFound))
    // True
    The second approach is perform a wholesale cut over to library based errors and bump the major version number of each library. The down side to this approach is bug fixes to our libraries will need to be integrated into multiple branches and forward merges will be a bit painful for new uses of any libraries. Example:
    const ErrorThingNotFound = errors.ConstError("thing not found")
    func MyLibraryFuncCheckThingX() error {
      // ...
      return errors.Errorf("%w some id", ErrorThingNotFound) 
    }
    
    // Juju Code
    err := MyLibraryFuncCheckThingX()
    fmt.Println(errors.Is(err, errors.NotFound))
    // False
    fmt.Println(errors.Is(err, mylib.ErrorThingNotFound))
    // True

Proposed New V2 Interface

The following is the proposed complete interface for the new v2 package:

type ConstError string

func (c ConstError) Error() string {}

type Error interface {
  error
  Add(error) Error
  Trace() Error
  Unwrap() error
}

func As(error, any) bool
func AsType[T error](err error) (T, bool) {}
func Errorf(string, ...any) Error {}
func ErrorStack(error) string {}
func HasType[T error](err error) bool {}
func Is(error, error) bool {}
func New(string) Error {}
func Join(errs ...error) Error {}
func Unwrap(error) error {}

mgo dependency?

go mod is picking up dependencies which are not used anywhere in the code.

eg following:

gopkg.in/mgo.v2

allow serialisation/deserialisation of *Err

I want juju errors to cross RPC boundaries and to do that I need some methods to serialize them on one side, and to recreate the error on the other from its serialized form. Can't do that now as all inputs are private, particularly 'file' and 'line' can only be set by SetLocation (and therefore using runtime.Caller values only), while I need them initialized to arbitrary values.

I have working code (and tests) for this, let me know if a PR is welcome or any concerns you may have about such changes.

Update yaml dependency to yaml.v3

Can we have the yaml dependency upgraded to yaml.v3? There is a known issue in v2: "Excessive Platform Resource Consumption within a Loop in Kubernetes", which is caught by Dependabot. It would be good to update to the latest version, if possible.

File paths are not being trimmed when project is outside of GOPATH (vgo)

in path.go there's a function trimGoPath() it relies on GOPATH variable to trim absolute paths when stacktrace is printed. With VGO the go source now can be outside of GOPATH, when that happens paths are no longer trimmed.

I could resolve the issue by setting srcDir to result of:

	_, file, _, _ := runtime.Caller(0)
	path := strings.TrimSuffix(file, "/main.go")

and have it working, unfortunately it is a private variable.

Perhaps there is a better way to get the path of the source code?

How to use NewNotFound() properly

Hi,

I am frequently using NewNotFound() and NewBadRequest() in my model logic so that the receiving controller knows about the type of error using an if-statement with errors.Is(err, errors.NotFound) for example.

I just realized now that errors which were wrapped with NewNotFound() somehow have no file, line and function recorded which makes it harder to find the error location.

Here is an example:

err := errors.New("my error")
err = errors.NewNotFound(err, "first context")
println(errors.ErrorStack(err))
       
// returns the following which lacks the file, line and function
first context: my error

Am I using it wrong?

Support go errors.Is(...), errors.As(...), implement error.Unwrap()

I use juju as a drop-in replacement for the base "errors" package. In all cases until now I could just swap out the import line and not worry about it, however with the addition of errors.Is and errors.As I've had to separately import the old errors package to keep those functions.

Can juju get .Is and .As aliases for the regular error package versions, and add a .Unwrap method to its errors?

I think the first two are trivial, but I believe .Unwrap is supposed to return the immediate parent, so it's not quite the same as juju's .Cause.

If this is reasonable I can make a PR.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.