goerror

package module
v0.0.0-...-8e7a8d8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 10, 2016 License: BSD-2-Clause Imports: 2 Imported by: 0

README

image

####stat star date 04 08 16

api               stable 
documentation     inlined godoc

##about

Package goerror provides for creation of semantic error types.

Using the stdlib error package, trying to discern the error type returned by a function, we can either (a) compare to a well-known error reference (for example, io.EOF), or (b) we have to parse the actual error message to distiguish between error flavors. The former approach (alal io.EOF) is fine for generic errors but obviously won't allow for call-site specific information in the error. And the latter approach is ad-hoc and fragile. (See motivating case below for elaboration.)

usage

defining an error 'type'
package example

import (
	"goerror"
)

// package errors
var (
    FooError = goerror.Define("Foo error")
    BarError = goerror.Define("Bar error")
    FubarError = goerror.Define("Fubar error")
)
using an error 'type'
package something

// note that we don't need to import goerror here.
import (
    "example"
)

func Doit(…) error {

    // creating an instance of example.Foo error
    if whynot() {
        return example.Foo("whynot")
    }

    // keep in mind details are optional 
    if because() {
        return example.Foo()
    }
    
    // and of course we could be returning another 
    // error flavor
    if allFubar() {
        return example.Fubar("blame it on murphy")
    }
    
    // and finally, an error may have an underlying cause,
    …, e := DoSubTask(..)
    if e != nil {
        return example.Fubar("failed to do it").WithCause(e)
    }
}
handling the error

With goerror we can distinguish between error types and also have call-site specific error details.

package elsewhere

import (
   "example"
   "goerror"
   "something"
)

if e := something.Doit(…); e != nil {
   switch typ := goerror.TypeOf(e); {
   case typ.Is(example.Foo):
   case typ.Is(example.Bar):
   case typ.Is(example.FooBar):
   default: 
      /* generic error */
   }
}

You can always check to see if an error has a cause.

// for example
if cause := e.Cause(); cause != nil {
    /* possibly attempt recover */
    if goerrors.TypeOf(cause).Is(Disconnected) {
        tryReconnect()
        goto retry
    }
}

Of course, you can also treat them like ordinary errors.

motivating case

For example, let's say we create an error for asserting input arg correctness, something like an AssertionError, or IllegalArgument. We want informative (read: useful) error messages, and we also want a consistent way of distinguishing between error types.

stdlib approach (I): use a global reference and pass that around

This approach allows for distinguishing between generic error types at the call-site, comparing references. But can you tell which of the arguments here is the one that causes the error?

define the error
// use a generic error
var IllegalArgument = errors.New("illegal argument error")
using the error
func foo(arg0 T, arg1 T2, …) error {
    if invalid(arg0) {
       return IllegalArgument
    }
    …
    if invalid(argn) {
       return IllegalArgument
    }
    …
    
    // and don't forget; may return other kinds of error here
    e := bar(…)
    if e != nil {
        return e
    }
    … 
}
handling the error

Here we can distinguish between generic error flavors (by comparing references) but

func foo(arg0 T, arg1 T2, …) error {
e := foo(a, b, c, … )
if e != nil {
    if e == IllegalArgument {
       /* which argument? */
    }
}
stdlib approach (II): parse the error message
define the error
// define the error prefix
const IllegalArgument = "illegal argument"
using the error
func foo(arg0 T, arg1 T2, …) error {
    if invalid(arg0) {
       return fmt.Errorf("%s: %s", IllegalArgument, "arg0")
    }
    …
    if invalid(argn) {
       return fmt.Errorf("%s: %s", IllegalArgument, "argn")
    }
    …
}
handling the error

Here we can no longer distinguish between generic error flavors (by comparing references) but can get some additional information.

func foo(arg0 T, arg1 T2, …) error {
e := foo(a, b, c, … )
if e != nil {
    errmsg := e.Error()
    // now parse the error message 
    ...
}

Documentation

Overview

package goerror provides for creation of semantic error types. Using the stdlib error package, when trying to discern the error type returned by a function, we can either (a) compare to a well-known error reference (for example, io.EOF), or (b) we have to parse the actual error message. The former, e.g. io.EOF, is fine for basic cases, but obviously won't allow for call-site specific information in the error.

For example, let's say we create an error for asserting input arg correctness, something like an AsssertError, or IllegalArgumentError. We can try pattern (a) per io.EOF, in which case we can certainly return that error, but can't provide additional info such as which precise arg caused the error. Or we can return a plain jane error with a formatted message, in which case we can't immediately tell what 'kind' of error was returned.

This package addresses this concern by providing error 'types' that can be generically defined at (some) package level and then used with explicit additional details.

Errors are created using 'Define'. (Note, not 'New', since this merely defines an error type).

var (
    TerribleError      = goerror.Define("TerribleError")
    NotSoTerribleError = goerror.Define("NotSoTerribleError")
)

Such error types can then be 'instantiated' using the defintion, wherever one would normally create and/or return a generic error.

// function foo may return either TerribleError or
// NotSoTerribleError
func foo() error {
    // ...
    if flipcoin() {
       return TerribleError("an example usage")
    }
    return NotSoTerribleError() // detailed info is optional
}

And in the functional callsite, we can specifically check to see what type of error we got.

if e := foo(); e != nil {
    switch typ := goerror.TypeOf(e); {
    case typ.Is(TerribleError):
        /* handle it */
    case typ.Is(NotSoTerribleError):
        /* handle it */
    }
}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Define

func Define(category string) errFn

defines a new categorical error.

Types

type Error

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

Note that this type is exported *only* in order to surface Is() to package docs. Otherwise, package users should not directly use this type.

Example

Example defining, returning, and checking goerror

package main

import (
	"fmt"
	"goerror"
)

// Let's define a few canonical goerror
var (
	IllegalArgument = goerror.Define("IllegalArgument")
	IllegalState    = goerror.Define("IllegalState")
	AccessDenied    = goerror.Define("AccessDenied")
	Bug             = goerror.Define("BUG")
)

// Example defining, returning, and checking goerror
func main() {

	user := "theuser"
	oldpw := "old-secret"
	newpw := "new-secret"

	if e := ChangePassword(user, oldpw, newpw); e != nil {
		switch typ := goerror.TypeOf(e); {
		case typ.Is(IllegalArgument): /* handle it */
		case typ.Is(IllegalState): /* handle it */
		case typ.Is(AccessDenied): /* handle it */
		default: /* this violates the API contract - must be a bug */
			panic(Bug(fmt.Sprintf("unexpected error %v returned by ChangePassword()", e)))
		}
	}
}

// (Example function that returns categorical goerror.)
// Change the user's password.
//
// returns IllegalArgument for any nil input;
// IllegalState if user not logged in;
// and AccessDenied if user and credentials don't match.
func ChangePassword(user, oldPassword, newPassword string) error {
	// assert args
	if user == "" {
		return IllegalArgument("user is nil")
	}
	if oldPassword == "" {
		return IllegalArgument("oldpassword is nil")
	}
	if newPassword == "" {
		return IllegalArgument("newPassword is nil")
	}

	// user must be already logged in to change passwords
	// (it's just an example ;-)
	if !UserLoggedIn(user) {
		return IllegalState("user must be logged in to change pw")
	}

	// verify user and oldpassword match
	// (Yes, bad idea to leak this info but it is an example of using
	//  root cause errors. cheer up.)
	if e := CheckAuthorized(user, oldPassword); e != nil {
		return AccessDenied().WithCause(e)
	}
	// ...

	return nil
}

func UserLoggedIn(user string) bool {
	// ...
	return false
}

func CheckAuthorized(user, pw string) error {
	// ...
	return fmt.Errorf("unauthorized")
}

func TypeOf

func TypeOf(e error) *Error

Returns an Error, typically for use in conjunction with the Error#Is(). Function name is as such to allow for a readable call site, as below:

if goerror.TypeOf(e).Is(AssertionError)

If the input arg 'e' is a plain (builtin) error, it is converted to a goerror.Error pointer.

func (*Error) Cause

func (e *Error) Cause() error

Returns associated cause, or nil.

func (*Error) Error

func (e *Error) Error() string

supports interface builtin.error

func (*Error) Is

func (e *Error) Is(efn errFn) bool

Returns true if the Error.error is an 'instance' of input arg 'errfn'.

func (*Error) WithCause

func (e *Error) WithCause(cause error) *Error

Associate a root cause error with the given error. If cause is already set, subsequent calls to this function are ignored.

Typcial usage pattern:

var WriteError = goerror.Define("Write Error")

func writeBuffer(..) error {
   ...
   // let's pretend we did io and got an error 'ioerror'
   return WriteError("in writeBuffer").WithCause(ioerror)
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL