GithubHelp home page GithubHelp logo

southclaws / fault Goto Github PK

View Code? Open in Web Editor NEW
158.0 5.0 7.0 676 KB

Go errors but structured and composable. Fault provides an extensible yet ergonomic mechanism for wrapping errors.

License: MIT License

Go 100.00%
errors golang go go-context structured-logging structured-errors go-errors

fault's Introduction

header

GoDoc Go Report Card codecov

Fault provides an extensible yet ergonomic mechanism for wrapping errors. It implements this as a kind of middleware style pattern of simple functions called decorators: func(error) error. A decorator simply wraps an error within another error, much like many libraries do.

What this facilitates is a simple, minimal and (most important) composable collection of error handling utilities designed to help you diagnose problems in your application logic without the need for overly verbose stack traces.

This is achieved by annotating errors with structured metadata instead of just gluing strings of text together. This approach plays nicely with structured logging tools as well as the existing Go errors ecosystem.

Usage

You can gradually adopt Fault into your codebase as it plays nicely with the existing Go error management ecosystem.

Wrapping errors

Wrapping errors as you ascend the call stack is essential to providing your team with adequate context when something goes wrong.

Simply wrap your errors as you would with any library:

if err != nil {
    return fault.Wrap(err)
}

What this gives you is basic stack traces. The philosophy behind stack traces provided by Fault is that you only care about relevant code locations. You do not need runtime/proc.go in your stack traces unless you're actually working on the Go compiler or some crazy low level tooling. A fault stack trace looks like this when formatted with %+v:

stdlib sentinel error
    /Users/southclaws/Work/fault/fault_test.go:34
failed to call function
    /Users/southclaws/Work/fault/fault_test.go:43
    /Users/southclaws/Work/fault/fault_test.go:52

And of course all of this information is accessible in a structured way so you can serialise it how you want for your logging stack of choice. Fault aims to be unopinionated about presentation.

But what if you want to add context? With pkg/errors and similar libraries, you often use errors.Wrap(err, "failed to do something") to add a bit of context to a wrapped error.

Fault provides something much more powerful, a mechanism to compose many wrappers together. The Wrap API in Fault accepts any number of functions that wrap the error with the signature func(error) error so the possibilities are endless.

if err != nil {
    return fault.Wrap(err, fmsg.With("failed to do a thing"))
}

This is how you add a message to an error in the same way you would with pkg/errors. Now this is quite verbose, lots of typing involved and if this is all you're going to do, you don't need to use Fault, you can just use fmt.Errorf and %w.

The power comes when you make use of some of the additional packages available as part of Fault:

if err != nil {
    return fault.Wrap(err,
        fctx.With(ctx), // decorate the error with key-value metadata from context
        ftag.With(ftag.NotFound), // categorise the error as a "not found"
        fmsg.With("failed to do something", "There was a technical error while retrieving your account"), // provide an end-user message
    )
}

You can also build your own utilities that work with the Fault option pattern. This is covered later in this document.

Handling errors

Wrapping errors is only half the story, eventually you'll need to actually handle the error (and no, return err is not "handling" an error, it's saying "I don't know what to do! Caller, you deal with this!".)

Utilities will provide their own way of extracting information from an error but Fault provides the Flatten function which allows you to access a simple stack trace and a reference to the root cause error.

chain := Flatten(err)

chain.Root

This is the root cause of the error chain. In other words, the error that was either created with errors.New (or similar) or some external error from another library not using Fault.

chain.Errors

This is the list of wrapped errors in the chain where the first item is the wrapper of the root cause.

error chain diagram

Utilities

Fault provides some utilities in subpackages to help you annotate and diagnose problems easily. Fault started its life as a single huge kitchen-sink style library but it quickly became quite bloated and developers rarely used everything it provided. This inspired the simple modular option-style design and each useful component was split into its own package.

It's worth noting that all of these utilities can be used on their own. If you don't want to use Fault's wrapping or stack traces you can simply import a library and use its Wrap function.

fmsg

end-user error example

This simple utility gives you the ability to decorate error chains with a separate set of messages intended for both developers and end-users to read.

The error messages returned by .Error() are always intended for developers to read. They are rarely exposed to end-users. When they are, it's usually fairly confusing and not a great user-experience.

You can use fmsg.With to wrap an error with an extra string of text, just like pkg/errors and similar:

err := errors.New("root")
err = fault.Wrap(err, fmsg.With("one"))
err = fault.Wrap(err, fmsg.With("two"))
err = fault.Wrap(err, fmsg.With("three"))
fmt.Println(err)
// three: two: one: root

As with any simple error wrapping library, the .Error() function simply joins these messages together with :.

However, in order to provide useful end-user error descriptions, you can use fmsg.WithDesc:

if err != nil {
    return fault.Wrap(err,
        fmsg.WithDesc("permission denied", "The post is not accessible from this account."),
    )
}

Once you're ready to render the human-readable errors to end-users, you simply call GetIssue:

issues := GetIssue(err)
// "The post is not accessible from this account."

Multiple wrapped issues are conjoined with a single space. Issue messages should end in a punctuation mark such as a period.

if err != nil {
    return fault.Wrap(err,
        fmsg.With("permission denied", "The category cannot be edited."),
    )
}

// later on

if err != nil {
    return fault.Wrap(err,
        fmsg.With("move post failed", "Could not move post to the specified category."),
    )
}

Yields:

issues := GetIssue(err)
// "Could not move post to the specified category. The category cannot be edited."

Which, while it reads much nicer than move post failed: permission denied, both messages are valuable to their individual target audiences.

Further reading on the topic of human-friendly error messages in this article.

fctx

fctx example

Error context is the missing link between the context package and errors. Contexts often contain bits of metadata about a call stack in large scale applications. Things like trace IDs, request IDs, user IDs, etc.

The problem is, once an error occurs and the stack starts to "unwind" (chained returns until the error is truly "handled" somewhere) the information stored in a context has already gone.

fctx gives you two tools to help understand the actual context around a problem.

Add metadata to a context

First, you decorate contexts with key-value data. Strings only and no nesting for simplicity.

ctx = fctx.WithMeta(ctx, "trace_id", traceID)

This stores the traceID value into the context. Conflicting keys will overwrite.

Note that while this function may look similar to context.WithValue in concept, it differs since it permits you to access all of the key-value as a single object for iterating later. When using context.WithValue you must know the exact keys. Now you could store a map but in order to add items to that map you would need to first read the map out, check if it exists, insert your key-value data and write it back.

Decorate errors with context metadata

When something goes wrong, all that metadata stored in a context can be copied into an error.

if err != nil {
    return fault.Wrap(err,
        fctx.With(ctx),
    )
}

Access metadata for logging

When your error chain is handled, you most likely want to log what happened. You can access key-value metadata from an error using Unwrap:

ec := fctx.Unwrap(err)
logger.Error("http handler failed", ec)

The Unwrap function returns a simple map[string]string value which you can use with your favourite structured logging tool. Now, instead of your logs looking like this:

{
  "error": "failed to create post 'My new post' in thread '1647152' by user '@southclaws' database connection refused"
}

Which is an absolute nightmare to search for when every error is worded differently...

They look like this:

{
  "error": "failed to create post: database connection refused",
  "metadata": {
    "title": "My new post",
    "thread_id": "1647152",
    "user_id": "@southclaws"
  }
}

Which is an absolute godsend when things go wrong.

ftag

This utility simply annotates an entire error chain with a single string. This facilitates categorising error chains with a simple token that allows mapping errors to response mechanisms such as HTTP status codes or gRPC status codes.

You can use the included error kinds which cover a wide variety of common categories of error or you can supply your own set of constants for your team/codebase.

if err != nil {
    return fault.Wrap(err,
        ftag.With(ftag.NotFound),
    )
}

Once you've annotated an error chain, you can use it later to determine which HTTP status to respond with:

ek := ftag.Get(err)
// ftag.NotFound = "NOT_FOUND"

switch ek {
  case ftag.NotFound:
    return http.StatusNotFound
  // others...
  default:
    return http.StatusInternalServerError
}

This removes the need to write verbose and explicit errors.Is checks on the error type to determine which type of HTTP status code to respond with.

Since the type Kind is just an alias to string, you can pass anything and switch on it.

Appendix

Rationale

The reason Fault came into existence was because I found nesting calls to various Wrap APIs was really awkward to write and read. The Golang errors ecosystem is diverse but unfortunately, composing together many small error related tools remains awkward due to the simple yet difficult to extend patterns set by the Golang standard library and popular error packages.

For example, to combine pkg/errors, tracerr and fctx you'd have to write:

fctx.Wrap(errors.Wrap(tracerr.Wrap(err), "failed to get user"), ctx)

Which is a bit of a nightmare to write (many nested calls) and a nightmare to read (not clear where the arguments start and end for each function). Because of this, it's not common to compose together libraries from the ecosystem.

Prior art

Building on the shoulders of giants, as is the open source way. Here are some great libraries I goodartistscopygreatartistssteal'd from:

Why "Fault"?

Because the word error is overused.

errors everywhere

fault's People

Contributors

matdurand avatar semihbkgr avatar southclaws avatar the-goodies avatar toqueteos 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

fault's Issues

Add snippets

Once API is v1, provide some snippets since I write errctx wrapping constantly:

{
  "if err != nil": {
    "prefix": "iferr",
    "body": [
      "if err != nil {",
      "\treturn nil, fault.Wrap(err, fctx.With(ctx))",
      "}"
    ],
    "description": "Snippet for if err != nil"
  }
}

Underlying map is modified globally in fctx

A confusing bug just occurred which was the result (I think, not fully investigated yet) of the map in a context chain being modified from deep in the call tree.

I assume this is because new contexts actually just store a pointer to the map, not a copy. So when fctx.WithMeta creates a new context, it's still pointing at the hash table from the parent context. Meaning when you add or change a field, you're actually just mutating a single map instance shared between all contexts.

Now this is kinda useful but it's too complex to deal with. The assumption should always be that contexts grow with the call tree as new children are added and are cleaned up as stack frames return. There should be no shared state and each ctx = somelib.WithXyz(ctx) should always create a copy of the data.

Add fault for sql queries and parameters

I think there is an advantage to have common error types directly in the fault library so that other libraries can throw a common fault that can be extracted from the error chain consistently.
I created such a wrapper that captures the SQL statement and parameters.

https://github.com/spearson78/fsql/blob/main/fsql.go

I propose that that withSql struct and the corresponding wrap and with methods be added to fault

This way other code that execute SQL can capture them and they can be logged consistently.

Not getting the expected output

Hi. I love the lib idea and would love to use this, but I'm not getting the expected output.

The first thing I'm not seeing would be multiple stacktrace lines. In the readme, you give this example

stdlib sentinel error
    /Users/southclaws/Work/fault/fault_test.go:34
failed to call function
    /Users/southclaws/Work/fault/fault_test.go:43
    /Users/southclaws/Work/fault/fault_test.go:52

where we see 2 lines for the "failed to call function" error stack (43 and 52), which leads me to believe this is possible.

The second thing I'm not seeing is the right line for my "fault root cause" error.

So what I'm seeing is

fault root cause error
        test/cmd/main.go:38 (this is a call to fault.Wrap, not where the error is created)
failed to call function
        test/cmd/main.go:29

what I would love to see

fault root cause error
        test/cmd/main.go:10 (this line is the line where we create the error using fault.New)
        test/cmd/main.go:14
        (maybe additional stacks here ... not sure how we would configure the depth)
??? (not sure what it should output since this is simple a call to fault.Wrap without additional context)
        test/cmd/main.go:34
failed to call function
        test/cmd/main.go:25

Am I using it wrong?

Here is the example program for reference

package main

import (
	"fmt"
	"github.com/Southclaws/fault"
	"github.com/Southclaws/fault/fmsg"
)

func rootCause1() error {
	return fault.New("fault root cause error")
}

func rootCause2() error {
	return rootCause1()
}

func ErrorCaller() error {
	err := errorCallerFromMiddleOfChain()
	if err != nil {
		return fault.Wrap(err)
	}

	return nil
}

func errorCallerFromMiddleOfChain() error {
	err := errorProducerFromRootCause()
	if err != nil {
		return fault.Wrap(err, fmsg.With("failed to call function"))
	}

	return nil
}

func errorProducerFromRootCause() error {
	err := rootCause2()
	if err != nil {
		return fault.Wrap(err)
	}

	return nil
}

func main() {
	err := ErrorCaller()
	fmt.Printf("%+v", err)
}

Thank you

Make fault.Wrap nil tolerant

I found it counter intuitive that fault.Wrap panicked when I passed a nil error.
I tried to upgrade my code by simply replacing

return x,err

with

return x,fault.Wrap(x,fctx.With(ctx))

However sometimes the err in my code is nil and fault.Wrap panicked.

For an error handling library it makes sense to me to not introduce panics into code.

I have these changes on my optional location branch (https://github.com/spearson78/fault/tree/floc). I can separate out the changes if needed.

Avoid dependencies

So there's nothing wrong with using testify/assert library if it floats your boat- though I feel there is so little use of it that the whole dependency could be ditched. Like ol' Go proverbs saying goes: a little copying is better than a little dependency :)

I can contribute this feature as I feel fault might have a big future ahead of itself. Awesome library by the way!

De-duplication logic causes some sentinel error patterns to result in error chain losses

fault/flatten.go

Lines 73 to 79 in 84c038e

// de-duplicate nested error messages
if next != nil {
if idx := strings.Index(message, next.Error()); idx != -1 {
// cut off the duplicate message and remove the separator.
message = strings.Trim(message[:idx], ": ")
}
}

Repro:

ErrPost = errors.New("post")

ErrExampleOne = fmt.Errorf("%w: %s", ErrPost, "example one")
ErrExampleTwo = fmt.Errorf("%w: %s", ErrExampleOne, "example two")

Expected chain:

post: example one: example two

What actually happens:

post

This one-word error message pattern seems to pop up sometimes, it's even in the standard library so it's definitely worth supporting.

Essentially what happens, I think, is that fmt.Errorf when used this way results in a single error with two messages nested within it:

ErrPost = "post"
ErrExampleOne = "post: example one"
ErrExampleTwo = "post: example one: example two"

So when you chain these together, you get a lot of duplication:

post: post: example one: post: example one: example two

Or, flattened:

"post"
"post: example one"
"post: example one: example two"

So when the de-duplication logic runs over this, it strips the 2 succeeding error messages because they begin with the first 4 bytes as the what the current iteration points to.

Which I guess is fine, as the actual chained error is mostly useless anyway, the Flatten output is what matters.

What I'd ideally want out of this is:

"post"
"example one"
"example two"

But, rolling all of these into one error will probably be simpler for now instead of trying to unwrap error messages into error chains where errors never existed. Otherwise you end up with:

"post"
"post"
"example one"
"post"
"example one"
"example two"

Pass ...Wrapper list to New constructor

A pattern I've seen pop up in usage of Fault is constructing a new error then immediately wrapping it.

image

For example, this is from a real codebase, we mostly use sentinel errors for fault.News but it still pops up from time to time.

return nil, fault.Wrap(fault.New("user account not found"),
	fctx.With(ctx),
	ftag.With(ftag.NotFound),
	fmsg.WithDesc("not found",
		"No account could be found with the specified email address."),

Two problems:

  • fault.Wrap(fault.New is noisy and verbose
  • "user account not found" + "not found" is pointless duplication
    • results in three container{} wraps
    • more writing
    • that issue is covered here: #32

Ideal interface:

return nil, fault.New("user account not found",
	fctx.With(ctx),
	ftag.With(ftag.NotFound),
	fmsg.With("No account could be found with the specified email address."),

Mainly what I'd like to tackle is giving fault.New a new signature:

func New(message string, w ...Wrapper) error

And immediately perform the wrapping internally if w contains any elements. This would allow you to create new errors and immediately pass them through your wrappers of choice without the need to first create the error then pass it to Wrap().

It would also make the wrapped sentinel pattern a bit less verbose too (a pattern for declaring sentinel errors with pre-determined wrappers such as error tags)

// Before
var (
    ErrEmptyPassword     = fault.Wrap(fault.New("password parameter is empty"), ftag.With(ftag.InvalidArgument))
    ErrUserNotFound      = fault.Wrap(fault.New("user account not found"), ftag.With(ftag.NotFound))
)

// After
var (
    ErrEmptyPassword     = fault.New("password parameter is empty", ftag.With(ftag.InvalidArgument))
    ErrUserNotFound      = fault.New("user account not found", ftag.With(ftag.NotFound))
)

Play nicely with multierror libraries

I'm honestly not super sure how this will work...

There was a case where a function iterated through a list of items and performed work that errored. The output error only contained one set of fctx metadata but ideally you'd have access to all error metadata.

Same goes for human-readable messages.

Make fault less opinionated on displaying "stack" trace and error messages together

It is great that the final err.Error() returns the "stack" lines and the error messages intermingled. It is quite opinionated though. Users might want them separately, so adding some helper functions to Chain could be good, like chain.Stack() to get the "stack" trace (separate from the errors) and chain.Error() to get just the error messages without the "stack" trace.

It might actually make more sense to just make err.Error() just return the error messages as per normal, like it would with normal golang error wrapping. Then chain.String() returns the combined format of "stack" lines plus error messages and then add a helper function like chain.Stack() to return just the "stack" lines.

Refactor Flatten to first flatten the tree and then filter the results

Currently, the implementation of Flatten is a bit more complex than it needs to be. This is partly due to the way the loop mutates next in a couple of places and also doubly calls Unwrap and re-assigns err twice.

This could be simplified by first running the Unwrap loop to product a simple slice of errors. Then, once there's a slice, it would be easier to do the filtering with lookahead/lookbehind instead of (what is essentially) double step/next iterations etc.

Redesign fmsg to be less verbose and more useful.

A common use-case for Fault is to wrap user-facing errors with fmsg.WithDesc where the second argument is a well-formed sentence of copy intended to be displayed to non-technical end-users.

One issue with this API is that it conflates internal error message wrapping with end-user copy.

It has two APIs:

  • fmsg.With for serving the same purpose as errors.Wrap(err, "message")
  • fmsg.WithDesc which does the same as above but with one extra argument: a message for the end-user.

This frequently results in code that looks like this:

return nil, fault.Wrap(fault.New("user account not found"),
	fmsg.WithDesc("not found",
		"No account could be found with the specified email address."),

And when the error is logged, the flattened form is:

not found: user account not found

Which is useless duplication of concepts, adding noise to the logs and making tracking issues more annoying.

There are two problems here:

  • When declaring a new error, you do not need to immediately provide a "contextual" internal error message
  • When you want to decorate an error chain with a message intended for the end-user, you're forced to provide a "contextual" internal error message

So I think it would be best if either fmsg was split into two packages or it provided two separate APIs for internal and external error messages. Something like:

return nil, fault.Wrap(fault.New("user account not found"),
	fmsg.WithExternal("No account could be found with the specified email address."),

For when you're creating a brand new error that already has an internal message (user account not found)

Or, for when you're wrapping an existing error and wish to provide some additional internal context as well as an external end-user message:

return nil, fault.Wrap(err,
	fmsg.WithInternal("failed to verify signature"),
	fmsg.WithExternal("Your signature could not be verified, please try again."),

I'm not set on WithInternal and WithExternal though, I'd prefer to keep short concise names that don't add too much noise to error decoration sites.

Support multiple ftag

Hey,

While using the lib, I discoved that it would be useful to support multiple ftags. Fault already allows me to set multiple ftags (Security being a custom one in this example)

err = fault.Wrap(err,
		fmsg.WithDesc("User not found", "Cannot find the requested user"),
		ftag.With(ftag.NotFound),
		ftag.With(Security))

but the API to get them only return the first tag. This is confusing since the code above is valid.

tag := ftag.Get(err) // will return Security in this case

If you define the solution, I can create a pull request.

More cleverly split up glued error messages

Currently, if you use a library that outputs wrapped errors, you end up with duplicates.

For example:

image

Results in:

errorf wrapped: stdlib sentinel error: stdlib sentinel error

The stdlib sentinel error part is duplicated, which is just noise when trying to read errors.

Solution? Not sure what the best course of action is, but it seems fairly simple to just use the next error message to de-duplicate/split the current.

For example, given:

[1] errorf wrapped: stdlib sentinel error
[2] stdlib sentinel error

When processing [1], look ahead to [2] and remove [2]'s nested message from [1], so we're left with:

[1] errorf wrapped
[2] stdlib sentinel error

Enable locations to be generated for the caller in libraries

If a library uses this fault package but the caller does not (yet) then the reported error line is within the library which is not so useful to the caller. It makes sense to me that libraries should report the location of the caller to make it easier for non fault code to get an accurate location.
I however think that In user code this should stay as the line where the wrap is generated to enable the user to identify the relevant return statement.
To handle these 2 situations I separated out location generation in my fork and ensure that only 1 location is created per fault.Wrap
I have some additional changes on my branch relating to nil handling that I can separate out if this issue is accepted but my nil handling suggestion not.

v1.0 Roadmap - consolidate built-in wrappers and implement API and DX improvements.

It's been about a year that this library has been in production use in multiple real-world revenue-generating products and I've got a good handle on the improvements I want to make and the direction this library is taking.

So, to mark this, I'd like to bundle the existing wrappers into a single package for a few reasons:

  • remove the individual packages: fctx, fmsg, ftag and combine them into one
  • introduce all the improvements listed in
    • #38
      • you'll be able to pass anything as a value, like stdlib slog.
    • #9
      • multi-error libraries will no longer cause confusion when unwrapping trees of errors
    • #25
      • add support for the new Go standard Unwrap() []error interface
    • #32
      • the "fmsg" (Fault message) library will only be concerned with end-user messages, not fmt.Errorf style internal messages
    • #31
      • No more Wrap(New(...), wrappers...) just New("msg", wrappers...) ๐Ÿฅณ
  • settle on the API designs for v1 and commit to compatibility going forward

I'd like to use a single nice short package name for all the built-in wrappers, I'm thinking of just fault/f or fault/w. Mostly just for aesthetics, similar to how Zap and other log aggregators work:

Settled on wrap for context, tags and user-friendly error message reference wrappers

return fault.Wrap(err,
        wrap.Ctx(ctx),
        wrap.Tag(fault.TagNotFound),
        wrap.Msg("There was a technical error while retrieving your account"),
)

Previous ideas:

return fault.Wrap(err,
        f.WithCtx(ctx), // decorate the error with key-value metadata from context
        f.WithTag(ftag.NotFound), // categorise the error as a "not found"
        f.WithMsg("There was a technical error while retrieving your account"), // provide an end-user message
)

Or with:

return fault.Wrap(err,
        with.Ctx(ctx), // decorate the error with key-value metadata from context
        with.Tag(ftag.NotFound), // categorise the error as a "not found"
        with.Msg("There was a technical error while retrieving your account"), // provide an end-user message
)

Or just w

toqueteos pointed out this would be a bad idea as single letter symbols are often used for variable and parameter names.

return fault.Wrap(err,
        w.Ctx(ctx), // decorate the error with key-value metadata from context
        w.Tag(ftag.NotFound), // categorise the error as a "not found"
        w.Msg("There was a technical error while retrieving your account"), // provide an end-user message
)

Any ideas for this are welcome though! cc @matdurand @the-goodies @semihbkgr @toqueteos (just tagging folks who have contributed in case you have ideas or reservations around this!)

Wrap should fail if err is nil

Pretty standard behaviour, most error wrapping libraries do this. An error being nil is usually the result of accidentally using an err value that's in scope due to an earlier error check in a context that's non-error-like:

err := nothing()
if err != nil {
  return err
}

if someCondition {
  // err is nil here due to the earlier check being false resulting in a nil err remaining in scope
  return errctx.Wrap(err, ctx)
}

func Wrap(err error, ctx context.Context, kv ...string) error {

Support arbitrary value type in fctx metadata

Hi, thanks for the library.

While testing the library, I see that fctx only supports string values. If I want to add a struct to the metadata, I'll have to either stringify it (but the log will be hard to read when logged using a structured logger) or add each field manually (which is painful if the struct has a lot of fields).

So I want to ask if there is any reason why fctx only supports string values, and if it is possible to make it support arbitrary-type (interface{}) values instead.

Error chains are not aware of "non message" error wrappers

Currently, Fault has this concept of a wrapper that has a message and a wrapper that is just a wrapper with metadata.

The wrappers that only provide metadata and do not provide a useful string message will show up in error chains like this:

<fctx>: <fctx>: account not found: <kind>: <fctx>: sql: no rows in result set

That's a lot of useless noise and is a tough sell despite the benefits of Fault decorators.

What's happening here is each decorator that only provides metadata has no information to return when Error() string is called. So they simply return their name in angle brackets.

So when a top-level Error method is called, and it joins together the entire error chain separated by : (as with most error libraries) you end up with a bunch of wrapper names in between the useful internal error messages.

Message missing due to incomplete Format implementation

In some cases, due to my lazy format implementation, you get no useful strings out of a Fault:

<fctx>
	.../db.go:419
<fctx>
	.../db.go:227

This I noticed when using assert.NoError(t, err) where err is a Fault chain.

Should be a simple fix, it seems NoError uses %+v when printing the error, same as panic does and others.

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.