Repository: uber-go/dig
Pull request #360 (2022-10-25 17:45:41)

Update Dig error types & reporting, more easily distinguish Dig errors vs. non-Dig errors
This update:
- Introduces the `dig.Error` interface, which is implemented by all errors returned by Dig, allowing users to use `errors.Is` and `errors.As` with Dig errors.
- Replaces the concept of errors causing other errors with Go's error wrapping conventions, so errors caused by other errors can now be unwrapped to their causing error with `errors.Unwrap`.
- Updates the behavior of `dig.RootCause` to return the first error non-`dig.Error` in the chain of errors passed to it, if there is one. Otherwise, it will return the first error that does not wrap another error (the bottom of the chain). In response to [this issue](https://github.com/uber-go/dig/issues/356), users can now do something like this:
```go
rootCause := dig.RootCause(err)
var de dig.Error
if errors.As(rootCause, &de) {
  // is a Dig error
} else {
  // is an error thrown by one of my provided or invoked functions
}
```
- Adds tests testing the new behavior of `dig.RootCause` in `error_test.go`

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/uber-go/dig?pullRequest=360) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/uber-go/dig?pullRequest=360) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/uber-go/dig?pullRequest=360) it.</sub>
codecov[bot]: # [Codecov](https://codecov.io/gh/uber-go/dig/pull/360?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) Report
> Merging [#360](https://codecov.io/gh/uber-go/dig/pull/360?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) (99200b2) into [master](https://codecov.io/gh/uber-go/dig/commit/601ea65abf18053c3a9f798cddd81de8749a841b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) (601ea65) will **decrease** coverage by `0.92%`.
> The diff coverage is `90.54%`.

```diff
@@            Coverage Diff             @@
##           master     #360      +/-   ##
==========================================
- Coverage   98.34%   97.42%   -0.93%     
==========================================
  Files          21       21              
  Lines        1450     1435      -15     
==========================================
- Hits         1426     1398      -28     
- Misses         15       28      +13     
  Partials        9        9              
```


| [Impacted Files](https://codecov.io/gh/uber-go/dig/pull/360?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) | Coverage Δ | |
|---|---|---|
| [group.go](https://codecov.io/gh/uber-go/dig/pull/360/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-Z3JvdXAuZ28=) | `91.66% <0.00%> (-8.34%)` | :arrow_down: |
| [cycle\_error.go](https://codecov.io/gh/uber-go/dig/pull/360/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-Y3ljbGVfZXJyb3IuZ28=) | `85.71% <71.42%> (-14.29%)` | :arrow_down: |
| [error.go](https://codecov.io/gh/uber-go/dig/pull/360/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZXJyb3IuZ28=) | `93.18% <78.57%> (-6.82%)` | :arrow_down: |
| [visualize.go](https://codecov.io/gh/uber-go/dig/pull/360/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-dmlzdWFsaXplLmdv) | `90.24% <91.66%> (+0.50%)` | :arrow_up: |
| [provide.go](https://codecov.io/gh/uber-go/dig/pull/360/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-cHJvdmlkZS5nbw==) | `99.47% <96.00%> (-0.53%)` | :arrow_down: |
| [decorate.go](https://codecov.io/gh/uber-go/dig/pull/360/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZGVjb3JhdGUuZ28=) | `100.00% <100.00%> (ø)` | |
| [inout.go](https://codecov.io/gh/uber-go/dig/pull/360/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-aW5vdXQuZ28=) | `100.00% <100.00%> (ø)` | |
| [invoke.go](https://codecov.io/gh/uber-go/dig/pull/360/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-aW52b2tlLmdv) | `100.00% <100.00%> (ø)` | |
| [param.go](https://codecov.io/gh/uber-go/dig/pull/360/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-cGFyYW0uZ28=) | `97.34% <100.00%> (-0.03%)` | :arrow_down: |
| [result.go](https://codecov.io/gh/uber-go/dig/pull/360/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-cmVzdWx0Lmdv) | `98.93% <100.00%> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go)

JacobOaks: I have pushed a new commit:
- combine `dig.Error` and `wrappedError` interfaces, remove `dummy()` methods
- rename `errSpecification` to `errInvalidInput`. I wanted to be careful to disambiguate the idea of user input error from errors specifically related to building arguments for functions in the Dig container (`errArgumentsFailed`), hence my avoiding using the term "argument".
- clean up code, bring up test coverage, address @sywhang's other points
JacobOaks: Update:
* RootCause() now more closely matches previous behavior: it will return first non-`dig.Error` or first error that does not wrap another error.
* There are now two interfaces for Dig errors. dig.Errors represents the public contract, and digError is an extension for private functionality (primarily error formatting). dig.Errors remains sealed.
* Add fields to structs in test tables

Patch:
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index b5ad357..ae5e46c 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -13,7 +13,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        go: ["1.17.x", "1.18.x", "1.19.x"]
+        go: ["1.18.x", "1.19.x"]
         include:
         - go: 1.19.x
           latest: true
diff --git a/cycle_error.go b/cycle_error.go
index 03b9434..a294aa9 100644
--- a/cycle_error.go
+++ b/cycle_error.go
@@ -22,7 +22,9 @@ package dig
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
+	"io"
 
 	"go.uber.org/dig/internal/digreflect"
 )
@@ -37,6 +39,8 @@ type errCycleDetected struct {
 	scope *Scope
 }
 
+var _ digError = errCycleDetected{}
+
 func (e errCycleDetected) Error() string {
 	// We get something like,
 	//
@@ -60,9 +64,16 @@ func (e errCycleDetected) Error() string {
 	return b.String()
 }
 
+func (e errCycleDetected) writeMessage(w io.Writer, v string) {
+	fmt.Fprint(w, e.Error())
+}
+
+func (e errCycleDetected) Format(w fmt.State, c rune) {
+	formatError(e, w, c)
+}
+
 // IsCycleDetected returns a boolean as to whether the provided error indicates
 // a cycle was detected in the container graph.
 func IsCycleDetected(err error) bool {
-	_, ok := RootCause(err).(errCycleDetected)
-	return ok
+	return errors.As(err, &errCycleDetected{})
 }
diff --git a/decorate.go b/decorate.go
index eb9c241..c7e79a6 100644
--- a/decorate.go
+++ b/decorate.go
@@ -21,7 +21,6 @@
 package dig
 
 import (
-	"errors"
 	"fmt"
 	"reflect"
 
@@ -224,10 +223,8 @@ func (s *Scope) Decorate(decorator interface{}, opts ...DecorateOption) error {
 	}
 	for _, k := range keys {
 		if _, ok := s.decorators[k]; ok {
-			return fmt.Errorf("cannot decorate using function %v: %s already decorated",
-				dn.dtype,
-				k,
-			)
+			return newErrInvalidInput(
+				fmt.Sprintf("cannot decorate using function %v: %s already decorated", dn.dtype, k), nil)
 		}
 		s.decorators[k] = dn
 	}
@@ -275,7 +272,7 @@ func findResultKeys(r resultList) ([]key, error) {
 			keys = append(keys, key{t: innerResult.Type, name: innerResult.Name})
 		case resultGrouped:
 			if innerResult.Type.Kind() != reflect.Slice {
-				return nil, errors.New("decorating a value group requires decorating the entire value group, not a single value")
+				return nil, newErrInvalidInput("decorating a value group requires decorating the entire value group, not a single value", nil)
 			}
 			keys = append(keys, key{t: innerResult.Type.Elem(), group: innerResult.Group})
 		case resultObject:
diff --git a/error.go b/error.go
index 3ee5a0a..430e488 100644
--- a/error.go
+++ b/error.go
@@ -27,36 +27,39 @@ import (
 	"reflect"
 	"sort"
 
-	"go.uber.org/dig/internal/digerror"
 	"go.uber.org/dig/internal/digreflect"
 	"go.uber.org/dig/internal/dot"
 )
 
-// Errors which know their underlying cause should implement this interface to
-// be compatible with RootCause.
+// Error is an interface implemented by all Dig errors.
 //
-// We use unexported methods because we don't want dig-internal causes to be
-// confused with the cause of the user-provided errors. For example, if we
-// used Unwrap(), then user-provided methods would also be unwrapped by
-// RootCause. We want RootCause to eliminate the dig error chain only.
-type causer interface {
-	fmt.Formatter
-
-	// Returns the next error in the chain.
-	cause() error
+// Use this interface, in conjunction with [RootCause], in order to
+// determine if errors you encounter come from Dig, or if they come
+// from provided constructors or invoked functions. See [RootCause]
+// for more info.
+type Error interface {
+	error
 
 	// Writes the message or context for this error in the chain.
 	//
+	// Note: the Error interface must always have a private function
+	// such as this one in order to maintain properly sealed.
+	//
 	// verb is either %v or %+v.
-	writeMessage(w io.Writer, verb string)
+	writeMessage(w io.Writer, v string)
 }
 
-// Implements fmt.Formatter for errors that implement causer.
-//
-// This Format method supports %v and %+v. In the %v form, the error is
-// printed on one line. In the %+v form, the error is split across multiple
-// lines on each error in the error chain.
-func formatCauser(c causer, w fmt.State, v rune) {
+// a digError is a dig.Error with additional functionality for
+// internal use - namely the ability to be formatted.
+type digError interface {
+	Error
+	fmt.Formatter
+}
+
+// formatError will call a dig.Error's writeMessage() method to print the error message
+// and then will automatically attempt to print errors wrapped underneath (which can create
+// a recursive effect if the wrapped error's Format() method then points back to this function).
+func formatError(e digError, w fmt.State, v rune) {
 	multiline := w.Flag('+') && v == 'v'
 	verb := "%v"
 	if multiline {
@@ -64,150 +67,79 @@ func formatCauser(c causer, w fmt.State, v rune) {
 	}
 
 	// "context: " or "context:\n"
-	c.writeMessage(w, verb)
+	e.writeMessage(w, verb)
+
+	// Will route back to this function recursively if next error
+	// is also wrapped and points back here
+	wrappedError := errors.Unwrap(e)
+	if wrappedError == nil {
+		return
+	}
 	io.WriteString(w, ":")
 	if multiline {
 		io.WriteString(w, "\n")
 	} else {
 		io.WriteString(w, " ")
 	}
-
-	fmt.Fprintf(w, verb, c.cause())
+	fmt.Fprintf(w, verb, wrappedError)
 }
 
-// RootCause returns the original error that caused the provided dig failure.
+// RootCause returns the first non-dig.Error in a chain of wrapped
+// errors, if there is one. Otherwise, RootCause returns the error
+// on the bottom of the chain of wrapped errors.
 //
-// RootCause may be used on errors returned by Invoke to get the original
-// error returned by a constructor or invoked function.
+// Use this function and errors.As to differentiate between Dig errors
+// and errors thrown by provided constructors or invoked functions:
+//
+//	rootCause := dig.RootCause(err)
+//	var de dig.Error
+//	if errors.As(rootCause, &de) {
+//	    // Is a Dig error
+//	} else {
+//	    // Is an error thrown by one of my provided or invoked functions
+//	}
 func RootCause(err error) error {
-	for {
-		if e, ok := err.(causer); ok {
-			err = e.cause()
-		} else {
-			return err
-		}
+	var de Error
+	// Dig down to first non dig.Error, or bottom of chain
+	for ; errors.As(err, &de); err = errors.Unwrap(de) {
 	}
-}
-
-// errf is a version of fmt.Errorf with support for a chain of multiple
-// formatted error messages.
-//
-// After msg, N arguments are consumed as formatting arguments for that
-// message, where N is the number of % symbols in msg. Following that, another
-// string may be added to become the next error in the chain. Each new error
-// is the `cause()` for the prior error.
-//
-//	 err := errf(
-//	   "could not process %v", thing,
-//	   "name %q is invalid", thing.Name,
-//	)
-//	fmt.Println(err)  // could not process Thing: name Foo is invalid
-//	fmt.Println(RootCause(err))  // name Foo is invalid
-//
-// In place of a string, the last error can be another error, in which case it
-// will be treated as the cause of the prior error chain.
-//
-//	 errf(
-//	   "could not process %v", thing,
-//	   "date %q could not be parsed", thing.Date,
-//	   parseError,
-//	)
-func errf(msg string, args ...interface{}) error {
-	// By implementing buildErrf as a closure rather than a standalone
-	// function, we're able to ensure that it is called only from errf, or
-	// from itself (recursively). By controlling these invocations in such
-	// a tight space, we are able to easily verify manually that we
-	// checked len(args) > 0 before making the call.
-	var buildErrf func([]interface{}) error
-	buildErrf = func(args []interface{}) error {
-		arg, args := args[0], args[1:] // assume len(args) > 0
-		if arg == nil {
-			digerror.BugPanicf("arg must not be nil")
-		}
-
-		switch v := arg.(type) {
-		case string:
-			need := numFmtArgs(v)
-			if len(args) < need {
-				digerror.BugPanicf("string %q needs %v arguments, got %v",
-					v,
-					need,
-					len(args))
-			}
-
-			msg := fmt.Sprintf(v, args[:need]...)
-			args := args[need:]
-
-			// If we don't have anything left to chain with, build the
-			// final error.
-			if len(args) == 0 {
-				return errors.New(msg)
-			}
 
-			return wrappedError{
-				msg: msg,
-				err: buildErrf(args),
-			}
-		case error:
-			if len(args) > 0 {
-				digerror.BugPanicf("error must be the last element but got %v", args)
-			}
-			return v
-		default:
-			digerror.BugPanicf("unexpected errf-argument type %T", arg)
-			return nil
-		}
+	if err == nil {
+		return de
 	}
 
-	// Prepend msg to the args list so that we can re-use the same
-	// args processing logic. The msg is a string just for type-safety of
-	// the first error.
-	newArgs := make([]interface{}, len(args)+1)
-	newArgs[0] = msg
-	copy(newArgs[1:], args)
-	return buildErrf(newArgs)
+	return err
 }
 
-// Returns the number of formatting arguments in the provided string. Does not
-// count escaped % symbols, specifically the string "%%".
-//
-//	fmt.Println(numFmtArgs("rate: %d%%"))  // 1
-func numFmtArgs(s string) int {
-	var (
-		count   int
-		percent bool // saw %
-	)
-	for _, c := range s {
-		if percent && c != '%' {
-			// Counts only if it's not a %%.
-			count++
-		}
-
-		// Next iteration should consider % only if the current %
-		// stands alone.
-		percent = !percent && c == '%'
-	}
-	return count
+// errInvalidInput is returned whenever the user provides bad input when
+// interacting with the container. May optionally have a more detailed
+// error wrapped underneath.
+type errInvalidInput struct {
+	Message string
+	Cause   error
 }
 
-type wrappedError struct {
-	err error
-	msg string
+var _ digError = errInvalidInput{}
+
+// newErrInvalidInput creates a new errInvalidInput, wrapping the given
+// other error that caused this error. If there is no underlying cause,
+// pass in nil. This will cause all attempts to unwrap this error to return
+// nil, replicating errors.Unwrap's behavior when passed an error without
+// an Unwrap() method.
+func newErrInvalidInput(msg string, cause error) errInvalidInput {
+	return errInvalidInput{msg, cause}
 }
 
-var _ causer = wrappedError{}
+func (e errInvalidInput) Error() string { return fmt.Sprint(e) }
 
-func (e wrappedError) cause() error {
-	return e.err
-}
+func (e errInvalidInput) Unwrap() error { return e.Cause }
 
-func (e wrappedError) writeMessage(w io.Writer, _ string) {
-	io.WriteString(w, e.msg)
+func (e errInvalidInput) writeMessage(w io.Writer, _ string) {
+	fmt.Fprintf(w, e.Message)
 }
 
-func (e wrappedError) Error() string { return fmt.Sprint(e) }
-func (e wrappedError) Format(w fmt.State, c rune) {
-	formatCauser(e, w, c)
+func (e errInvalidInput) Format(w fmt.State, c rune) {
+	formatError(e, w, c)
 }
 
 // errProvide is returned when a constructor could not be Provided into the
@@ -217,19 +149,18 @@ type errProvide struct {
 	Reason error
 }
 
-var _ causer = errProvide{}
+var _ digError = errProvide{}
 
-func (e errProvide) cause() error {
-	return e.Reason
-}
+func (e errProvide) Error() string { return fmt.Sprint(e) }
+
+func (e errProvide) Unwrap() error { return e.Reason }
 
 func (e errProvide) writeMessage(w io.Writer, verb string) {
 	fmt.Fprintf(w, "cannot provide function "+verb, e.Func)
 }
 
-func (e errProvide) Error() string { return fmt.Sprint(e) }
 func (e errProvide) Format(w fmt.State, c rune) {
-	formatCauser(e, w, c)
+	formatError(e, w, c)
 }
 
 // errConstructorFailed is returned when a user-provided constructor failed
@@ -239,19 +170,18 @@ type errConstructorFailed struct {
 	Reason error
 }
 
-var _ causer = errConstructorFailed{}
+var _ digError = errConstructorFailed{}
 
-func (e errConstructorFailed) cause() error {
-	return e.Reason
-}
+func (e errConstructorFailed) Error() string { return fmt.Sprint(e) }
+
+func (e errConstructorFailed) Unwrap() error { return e.Reason }
 
 func (e errConstructorFailed) writeMessage(w io.Writer, verb string) {
 	fmt.Fprintf(w, "received non-nil error from function "+verb, e.Func)
 }
 
-func (e errConstructorFailed) Error() string { return fmt.Sprint(e) }
 func (e errConstructorFailed) Format(w fmt.State, c rune) {
-	formatCauser(e, w, c)
+	formatError(e, w, c)
 }
 
 // errArgumentsFailed is returned when a function could not be run because one
@@ -261,19 +191,18 @@ type errArgumentsFailed struct {
 	Reason error
 }
 
-var _ causer = errArgumentsFailed{}
+var _ digError = errArgumentsFailed{}
 
-func (e errArgumentsFailed) cause() error {
-	return e.Reason
-}
+func (e errArgumentsFailed) Error() string { return fmt.Sprint(e) }
+
+func (e errArgumentsFailed) Unwrap() error { return e.Reason }
 
 func (e errArgumentsFailed) writeMessage(w io.Writer, verb string) {
 	fmt.Fprintf(w, "could not build arguments for function "+verb, e.Func)
 }
 
-func (e errArgumentsFailed) Error() string { return fmt.Sprint(e) }
 func (e errArgumentsFailed) Format(w fmt.State, c rune) {
-	formatCauser(e, w, c)
+	formatError(e, w, c)
 }
 
 // errMissingDependencies is returned when the dependencies of a function are
@@ -283,19 +212,18 @@ type errMissingDependencies struct {
 	Reason error
 }
 
-var _ causer = errMissingDependencies{}
+var _ digError = errMissingDependencies{}
 
-func (e errMissingDependencies) cause() error {
-	return e.Reason
-}
+func (e errMissingDependencies) Error() string { return fmt.Sprint(e) }
+
+func (e errMissingDependencies) Unwrap() error { return e.Reason }
 
 func (e errMissingDependencies) writeMessage(w io.Writer, verb string) {
 	fmt.Fprintf(w, "missing dependencies for function "+verb, e.Func)
 }
 
-func (e errMissingDependencies) Error() string { return fmt.Sprint(e) }
 func (e errMissingDependencies) Format(w fmt.State, c rune) {
-	formatCauser(e, w, c)
+	formatError(e, w, c)
 }
 
 // errParamSingleFailed is returned when a paramSingle could not be built.
@@ -305,19 +233,18 @@ type errParamSingleFailed struct {
 	CtorID dot.CtorID
 }
 
-var _ causer = errParamSingleFailed{}
+var _ digError = errParamSingleFailed{}
 
-func (e errParamSingleFailed) cause() error {
-	return e.Reason
-}
+func (e errParamSingleFailed) Error() string { return fmt.Sprint(e) }
+
+func (e errParamSingleFailed) Unwrap() error { return e.Reason }
 
 func (e errParamSingleFailed) writeMessage(w io.Writer, _ string) {
 	fmt.Fprintf(w, "failed to build %v", e.Key)
 }
 
-func (e errParamSingleFailed) Error() string { return fmt.Sprint(e) }
 func (e errParamSingleFailed) Format(w fmt.State, c rune) {
-	formatCauser(e, w, c)
+	formatError(e, w, c)
 }
 
 func (e errParamSingleFailed) updateGraph(g *dot.Graph) {
@@ -339,19 +266,18 @@ type errParamGroupFailed struct {
 	CtorID dot.CtorID
 }
 
-var _ causer = errParamGroupFailed{}
+var _ digError = errParamGroupFailed{}
 
-func (e errParamGroupFailed) cause() error {
-	return e.Reason
-}
+func (e errParamGroupFailed) Error() string { return fmt.Sprint(e) }
+
+func (e errParamGroupFailed) Unwrap() error { return e.Reason }
 
 func (e errParamGroupFailed) writeMessage(w io.Writer, _ string) {
 	fmt.Fprintf(w, "could not build value group %v", e.Key)
 }
 
-func (e errParamGroupFailed) Error() string { return fmt.Sprint(e) }
 func (e errParamGroupFailed) Format(w fmt.State, c rune) {
-	formatCauser(e, w, c)
+	formatError(e, w, c)
 }
 
 func (e errParamGroupFailed) updateGraph(g *dot.Graph) {
@@ -424,6 +350,8 @@ func (mt missingType) Format(w fmt.State, v rune) {
 // Multiple instances of this error may be merged together by appending them.
 type errMissingTypes []missingType // inv: len > 0
 
+var _ digError = errMissingTypes(nil)
+
 func newErrMissingTypes(c containerStore, k key) errMissingTypes {
 	// Possible types we will look for in the container. We will always look
 	// for pointers to the requested type and some extras on a per-Kind basis.
@@ -468,12 +396,11 @@ func newErrMissingTypes(c containerStore, k key) errMissingTypes {
 	return errMissingTypes{mt}
 }
 
-func (e errMissingTypes) Error() string {
-	return fmt.Sprint(e)
-}
+func (e errMissingTypes) Error() string { return fmt.Sprint(e) }
 
-func (e errMissingTypes) Format(w fmt.State, v rune) {
-	multiline := w.Flag('+') && v == 'v'
+func (e errMissingTypes) writeMessage(w io.Writer, v string) {
+
+	multiline := v == "%+v"
 
 	if len(e) == 1 {
 		io.WriteString(w, "missing type:")
@@ -502,6 +429,10 @@ func (e errMissingTypes) Format(w fmt.State, v rune) {
 	}
 }
 
+func (e errMissingTypes) Format(w fmt.State, c rune) {
+	formatError(e, w, c)
+}
+
 func (e errMissingTypes) updateGraph(g *dot.Graph) {
 	missing := make([]*dot.Result, len(e))
 
diff --git a/go.mod b/go.mod
index 1b23da2..ff19a98 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module go.uber.org/dig
 
-go 1.17
+go 1.18
 
 require github.com/stretchr/testify v1.7.1
 
diff --git a/group.go b/group.go
index 819ee82..b513611 100644
--- a/group.go
+++ b/group.go
@@ -22,6 +22,7 @@ package dig
 
 import (
 	"fmt"
+	"io"
 	"strings"
 )
 
@@ -37,8 +38,16 @@ type group struct {
 
 type errInvalidGroupOption struct{ Option string }
 
-func (e errInvalidGroupOption) Error() string {
-	return fmt.Sprintf("invalid option %q", e.Option)
+var _ digError = errInvalidGroupOption{}
+
+func (e errInvalidGroupOption) Error() string { return fmt.Sprint(e) }
+
+func (e errInvalidGroupOption) writeMessage(w io.Writer, v string) {
+	fmt.Fprintf(w, "invalid option %q", e.Option)
+}
+
+func (e errInvalidGroupOption) Format(w fmt.State, c rune) {
+	formatError(e, w, c)
 }
 
 func parseGroupString(s string) (group, error) {
diff --git a/inout.go b/inout.go
index 3c2e17b..3d57584 100644
--- a/inout.go
+++ b/inout.go
@@ -22,6 +22,7 @@ package dig
 
 import (
 	"container/list"
+	"fmt"
 	"reflect"
 	"strconv"
 )
@@ -166,9 +167,8 @@ func isFieldOptional(f reflect.StructField) (bool, error) {
 
 	optional, err := strconv.ParseBool(tag)
 	if err != nil {
-		err = errf(
-			"invalid value %q for %q tag on field %v",
-			tag, _optionalTag, f.Name, err)
+		err = newErrInvalidInput(
+			fmt.Sprintf("invalid value %q for %q tag on field %v", tag, _optionalTag, f.Name), err)
 	}
 
 	return optional, err
diff --git a/invoke.go b/invoke.go
index e154bdd..69a2f70 100644
--- a/invoke.go
+++ b/invoke.go
@@ -21,7 +21,7 @@
 package dig
 
 import (
-	"errors"
+	"fmt"
 	"reflect"
 
 	"go.uber.org/dig/internal/digreflect"
@@ -57,10 +57,11 @@ func (c *Container) Invoke(function interface{}, opts ...InvokeOption) error {
 func (s *Scope) Invoke(function interface{}, opts ...InvokeOption) error {
 	ftype := reflect.TypeOf(function)
 	if ftype == nil {
-		return errors.New("can't invoke an untyped nil")
+		return newErrInvalidInput("can't invoke an untyped nil", nil)
 	}
 	if ftype.Kind() != reflect.Func {
-		return errf("can't invoke non-function %v (type %v)", function, ftype)
+		return newErrInvalidInput(
+			fmt.Sprintf("can't invoke non-function %v (type %v)", function, ftype), nil)
 	}
 
 	pl, err := newParamList(ftype, s)
@@ -77,7 +78,7 @@ func (s *Scope) Invoke(function interface{}, opts ...InvokeOption) error {
 
 	if !s.isVerifiedAcyclic {
 		if ok, cycle := graph.IsAcyclic(s.gh); !ok {
-			return errf("cycle detected in dependency graph", s.cycleDetectedError(cycle))
+			return newErrInvalidInput("cycle detected in dependency graph", s.cycleDetectedError(cycle))
 		}
 		s.isVerifiedAcyclic = true
 	}
diff --git a/param.go b/param.go
index e7d0a8e..d584fc2 100644
--- a/param.go
+++ b/param.go
@@ -21,7 +21,6 @@
 package dig
 
 import (
-	"errors"
 	"fmt"
 	"reflect"
 	"strconv"
@@ -68,17 +67,16 @@ var (
 func newParam(t reflect.Type, c containerStore) (param, error) {
 	switch {
 	case IsOut(t) || (t.Kind() == reflect.Ptr && IsOut(t.Elem())) || embedsType(t, _outPtrType):
-		return nil, errf("cannot depend on result objects", "%v embeds a dig.Out", t)
+		return nil, newErrInvalidInput(fmt.Sprintf(
+			"cannot depend on result objects: %v embeds a dig.Out", t), nil)
 	case IsIn(t):
 		return newParamObject(t, c)
 	case embedsType(t, _inPtrType):
-		return nil, errf(
-			"cannot build a parameter object by embedding *dig.In, embed dig.In instead",
-			"%v embeds *dig.In", t)
+		return nil, newErrInvalidInput(fmt.Sprintf(
+			"cannot build a parameter object by embedding *dig.In, embed dig.In instead: %v embeds *dig.In", t), nil)
 	case t.Kind() == reflect.Ptr && IsIn(t.Elem()):
-		return nil, errf(
-			"cannot depend on a pointer to a parameter object, use a value instead",
-			"%v is a pointer to a struct that embeds dig.In", t)
+		return nil, newErrInvalidInput(fmt.Sprintf(
+			"cannot depend on a pointer to a parameter object, use a value instead: %v is a pointer to a struct that embeds dig.In", t), nil)
 	default:
 		return paramSingle{Type: t}, nil
 	}
@@ -130,7 +128,7 @@ func newParamList(ctype reflect.Type, c containerStore) (paramList, error) {
 	for i := 0; i < numArgs; i++ {
 		p, err := newParam(ctype.In(i), c)
 		if err != nil {
-			return pl, errf("bad argument %d", i+1, err)
+			return pl, newErrInvalidInput(fmt.Sprintf("bad argument %d", i+1), err)
 		}
 		pl.Params = append(pl.Params, p)
 	}
@@ -387,7 +385,8 @@ func newParamObject(t reflect.Type, c containerStore) (paramObject, error) {
 		}
 		pof, err := newParamObjectField(i, f, c)
 		if err != nil {
-			return po, errf("bad field %q of %v", f.Name, t, err)
+			return po, newErrInvalidInput(
+				fmt.Sprintf("bad field %q of %v", f.Name, t), err)
 		}
 		po.Fields = append(po.Fields, pof)
 	}
@@ -447,9 +446,8 @@ func newParamObjectField(idx int, f reflect.StructField, c containerStore) (para
 	var p param
 	switch {
 	case f.PkgPath != "":
-		return pof, errf(
-			"unexported fields not allowed in dig.In, did you mean to export %q (%v)?",
-			f.Name, f.Type)
+		return pof, newErrInvalidInput(
+			fmt.Sprintf("unexported fields not allowed in dig.In, did you mean to export %q (%v)?", f.Name, f.Type), nil)
 
 	case f.Tag.Get(_groupTag) != "":
 		var err error
@@ -543,18 +541,16 @@ func newParamGroupedSlice(f reflect.StructField, c containerStore) (paramGrouped
 	optional, _ := isFieldOptional(f)
 	switch {
 	case f.Type.Kind() != reflect.Slice:
-		return pg, errf("value groups may be consumed as slices only",
-			"field %q (%v) is not a slice", f.Name, f.Type)
+		return pg, newErrInvalidInput(
+			fmt.Sprintf("value groups may be consumed as slices only: field %q (%v) is not a slice", f.Name, f.Type), nil)
 	case g.Flatten:
-		return pg, errf("cannot use flatten in parameter value groups",
-			"field %q (%v) specifies flatten", f.Name, f.Type)
+		return pg, newErrInvalidInput(
+			fmt.Sprintf("cannot use flatten in parameter value groups: field %q (%v) specifies flatten", f.Name, f.Type), nil)
 	case name != "":
-		return pg, errf(
-			"cannot use named values with value groups",
-			"name:%q requested with group:%q", name, pg.Group)
-
+		return pg, newErrInvalidInput(
+			fmt.Sprintf("cannot use named values with value groups: name:%q requested with group:%q", name, pg.Group), nil)
 	case optional:
-		return pg, errors.New("value groups cannot be optional")
+		return pg, newErrInvalidInput("value groups cannot be optional", nil)
 	}
 	c.newGraphNode(&pg, pg.orders)
 	return pg, nil
@@ -664,9 +660,8 @@ func isIgnoreUnexportedSet(f reflect.StructField) (bool, error) {
 
 	allowed, err := strconv.ParseBool(tag)
 	if err != nil {
-		err = errf(
-			"invalid value %q for %q tag on field %v",
-			tag, _ignoreUnexportedTag, f.Name, err)
+		err = newErrInvalidInput(
+			fmt.Sprintf("invalid value %q for %q tag on field %v", tag, _ignoreUnexportedTag, f.Name), err)
 	}
 
 	return allowed, err
diff --git a/provide.go b/provide.go
index 1cf808c..905c7e9 100644
--- a/provide.go
+++ b/provide.go
@@ -22,7 +22,6 @@ package dig
 
 import (
 	"bytes"
-	"errors"
 	"fmt"
 	"reflect"
 	"strings"
@@ -49,12 +48,12 @@ type provideOptions struct {
 func (o *provideOptions) Validate() error {
 	if len(o.Group) > 0 {
 		if len(o.Name) > 0 {
-			return fmt.Errorf(
-				"cannot use named values with value groups: name:%q provided with group:%q", o.Name, o.Group)
+			return newErrInvalidInput(
+				fmt.Sprintf("cannot use named values with value groups: name:%q provided with group:%q", o.Name, o.Group), nil)
 		}
 		if len(o.As) > 0 {
-			return fmt.Errorf(
-				"cannot use dig.As with value groups: dig.As provided with group:%q", o.Group)
+			return newErrInvalidInput(
+				fmt.Sprintf("cannot use dig.As with value groups: dig.As provided with group:%q", o.Group), nil)
 		}
 	}
 
@@ -63,26 +62,30 @@ func (o *provideOptions) Validate() error {
 	// https://golang.org/ref/spec#raw_string_lit is that they cannot contain
 	// backquotes.
 	if strings.ContainsRune(o.Name, '`') {
-		return errf("invalid dig.Name(%q): names cannot contain backquotes", o.Name)
+		return newErrInvalidInput(
+			fmt.Sprintf("invalid dig.Name(%q): names cannot contain backquotes", o.Name), nil)
 	}
 	if strings.ContainsRune(o.Group, '`') {
-		return errf("invalid dig.Group(%q): group names cannot contain backquotes", o.Group)
+		return newErrInvalidInput(
+			fmt.Sprintf("invalid dig.Group(%q): group names cannot contain backquotes", o.Group), nil)
 	}
 
 	for _, i := range o.As {
 		t := reflect.TypeOf(i)
 
 		if t == nil {
-			return fmt.Errorf("invalid dig.As(nil): argument must be a pointer to an interface")
+			return newErrInvalidInput("invalid dig.As(nil): argument must be a pointer to an interface", nil)
 		}
 
 		if t.Kind() != reflect.Ptr {
-			return fmt.Errorf("invalid dig.As(%v): argument must be a pointer to an interface", t)
+			return newErrInvalidInput(
+				fmt.Sprintf("invalid dig.As(%v): argument must be a pointer to an interface", t), nil)
 		}
 
 		pointingTo := t.Elem()
 		if pointingTo.Kind() != reflect.Interface {
-			return fmt.Errorf("invalid dig.As(*%v): argument must be a pointer to an interface", pointingTo)
+			return newErrInvalidInput(
+				fmt.Sprintf("invalid dig.As(*%v): argument must be a pointer to an interface", pointingTo), nil)
 		}
 	}
 	return nil
@@ -405,10 +408,11 @@ func (c *Container) Provide(constructor interface{}, opts ...ProvideOption) erro
 func (s *Scope) Provide(constructor interface{}, opts ...ProvideOption) error {
 	ctype := reflect.TypeOf(constructor)
 	if ctype == nil {
-		return errors.New("can't provide an untyped nil")
+		return newErrInvalidInput("can't provide an untyped nil", nil)
 	}
 	if ctype.Kind() != reflect.Func {
-		return errf("must provide constructor function, got %v (type %v)", constructor, ctype)
+		return newErrInvalidInput(
+			fmt.Sprintf("must provide constructor function, got %v (type %v)", constructor, ctype), nil)
 	}
 
 	var options provideOptions
@@ -480,7 +484,8 @@ func (s *Scope) provide(ctor interface{}, opts provideOptions) (err error) {
 
 	ctype := reflect.TypeOf(ctor)
 	if len(keys) == 0 {
-		return errf("%v must provide at least one non-error type", ctype)
+		return newErrInvalidInput(
+			fmt.Sprintf("%v must provide at least one non-error type", ctype), nil)
 	}
 
 	oldProviders := make(map[key][]*constructorNode)
@@ -503,7 +508,7 @@ func (s *Scope) provide(ctor interface{}, opts provideOptions) (err error) {
 				s.providers[k] = ops
 			}
 
-			return errf("this function introduces a cycle", s.cycleDetectedError(cycle))
+			return newErrInvalidInput("this function introduces a cycle", s.cycleDetectedError(cycle))
 		}
 		s.isVerifiedAcyclic = true
 	}
@@ -642,10 +647,8 @@ func (cv connectionVisitor) Visit(res result) resultVisitor {
 func (cv connectionVisitor) checkKey(k key, path string) error {
 	defer func() { cv.keyPaths[k] = path }()
 	if conflict, ok := cv.keyPaths[k]; ok {
-		return errf(
-			"cannot provide %v from %v", k, path,
-			"already provided by %v", conflict,
-		)
+		return newErrInvalidInput(fmt.Sprintf("cannot provide %v from %v", k, path),
+			newErrInvalidInput(fmt.Sprintf("already provided by %v", conflict), nil))
 	}
 	if ps := cv.s.providers[k]; len(ps) > 0 {
 		cons := make([]string, len(ps))
@@ -653,10 +656,8 @@ func (cv connectionVisitor) checkKey(k key, path string) error {
 			cons[i] = fmt.Sprint(p.Location())
 		}
 
-		return errf(
-			"cannot provide %v from %v", k, path,
-			"already provided by %v", strings.Join(cons, "; "),
-		)
+		return newErrInvalidInput(fmt.Sprintf("cannot provide %v from %v", k, path),
+			newErrInvalidInput(fmt.Sprintf("already provided by %v", strings.Join(cons, "; ")), nil))
 	}
 	return nil
 }
diff --git a/result.go b/result.go
index 38d73c0..2e07d8c 100644
--- a/result.go
+++ b/result.go
@@ -21,7 +21,6 @@
 package dig
 
 import (
-	"errors"
 	"fmt"
 	"reflect"
 
@@ -70,35 +69,33 @@ type resultOptions struct {
 func newResult(t reflect.Type, opts resultOptions) (result, error) {
 	switch {
 	case IsIn(t) || (t.Kind() == reflect.Ptr && IsIn(t.Elem())) || embedsType(t, _inPtrType):
-		return nil, errf("cannot provide parameter objects", "%v embeds a dig.In", t)
+		return nil, newErrInvalidInput(fmt.Sprintf(
+			"cannot provide parameter objects: %v embeds a dig.In", t), nil)
 	case isError(t):
-		return nil, errf("cannot return an error here, return it from the constructor instead")
+		return nil, newErrInvalidInput("cannot return an error here, return it from the constructor instead", nil)
 	case IsOut(t):
 		return newResultObject(t, opts)
 	case embedsType(t, _outPtrType):
-		return nil, errf(
-			"cannot build a result object by embedding *dig.Out, embed dig.Out instead",
-			"%v embeds *dig.Out", t)
+		return nil, newErrInvalidInput(fmt.Sprintf(
+			"cannot build a result object by embedding *dig.Out, embed dig.Out instead: %v embeds *dig.Out", t), nil)
 	case t.Kind() == reflect.Ptr && IsOut(t.Elem()):
-		return nil, errf(
-			"cannot return a pointer to a result object, use a value instead",
-			"%v is a pointer to a struct that embeds dig.Out", t)
+		return nil, newErrInvalidInput(fmt.Sprintf(
+			"cannot return a pointer to a result object, use a value instead: %v is a pointer to a struct that embeds dig.Out", t), nil)
 	case len(opts.Group) > 0:
 		g, err := parseGroupString(opts.Group)
 		if err != nil {
-			return nil, errf(
-				"cannot parse group %q", opts.Group, err)
+			return nil, newErrInvalidInput(
+				fmt.Sprintf("cannot parse group %q", opts.Group), err)
 		}
 		rg := resultGrouped{Type: t, Group: g.Name, Flatten: g.Flatten}
 		if g.Soft {
-			return nil, errf("cannot use soft with result value groups",
-				"soft was used with group:%q", g.Name)
+			return nil, newErrInvalidInput(fmt.Sprintf(
+				"cannot use soft with result value groups: soft was used with group:%q", g.Name), nil)
 		}
 		if g.Flatten {
 			if t.Kind() != reflect.Slice {
-				return nil, errf(
-					"flatten can be applied to slices only",
-					"%v is not a slice", t)
+				return nil, newErrInvalidInput(fmt.Sprintf(
+					"flatten can be applied to slices only: %v is not a slice", t), nil)
 			}
 			rg.Type = rg.Type.Elem()
 		}
@@ -214,7 +211,7 @@ func newResultList(ctype reflect.Type, opts resultOptions) (resultList, error) {
 
 		r, err := newResult(t, opts)
 		if err != nil {
-			return rl, errf("bad result %d", i+1, err)
+			return rl, newErrInvalidInput(fmt.Sprintf("bad result %d", i+1), err)
 		}
 
 		rl.Results = append(rl.Results, r)
@@ -274,7 +271,8 @@ func newResultSingle(t reflect.Type, opts resultOptions) (resultSingle, error) {
 			continue
 		}
 		if !t.Implements(ifaceType) {
-			return r, fmt.Errorf("invalid dig.As: %v does not implement %v", t, ifaceType)
+			return r, newErrInvalidInput(
+				fmt.Sprintf("invalid dig.As: %v does not implement %v", t, ifaceType), nil)
 		}
 		asTypes = append(asTypes, ifaceType)
 	}
@@ -340,13 +338,13 @@ func (ro resultObject) DotResult() []*dot.Result {
 func newResultObject(t reflect.Type, opts resultOptions) (resultObject, error) {
 	ro := resultObject{Type: t}
 	if len(opts.Name) > 0 {
-		return ro, errf(
-			"cannot specify a name for result objects", "%v embeds dig.Out", t)
+		return ro, newErrInvalidInput(fmt.Sprintf(
+			"cannot specify a name for result objects: %v embeds dig.Out", t), nil)
 	}
 
 	if len(opts.Group) > 0 {
-		return ro, errf(
-			"cannot specify a group for result objects", "%v embeds dig.Out", t)
+		return ro, newErrInvalidInput(fmt.Sprintf(
+			"cannot specify a group for result objects: %v embeds dig.Out", t), nil)
 	}
 
 	for i := 0; i < t.NumField(); i++ {
@@ -358,7 +356,7 @@ func newResultObject(t reflect.Type, opts resultOptions) (resultObject, error) {
 
 		rof, err := newResultObjectField(i, f, opts)
 		if err != nil {
-			return ro, errf("bad field %q of %v", f.Name, t, err)
+			return ro, newErrInvalidInput(fmt.Sprintf("bad field %q of %v", f.Name, t), err)
 		}
 
 		ro.Fields = append(ro.Fields, rof)
@@ -402,8 +400,8 @@ func newResultObjectField(idx int, f reflect.StructField, opts resultOptions) (r
 	var r result
 	switch {
 	case f.PkgPath != "":
-		return rof, errf(
-			"unexported fields not allowed in dig.Out, did you mean to export %q (%v)?", f.Name, f.Type)
+		return rof, newErrInvalidInput(
+			fmt.Sprintf("unexported fields not allowed in dig.Out, did you mean to export %q (%v)?", f.Name, f.Type), nil)
 
 	case f.Tag.Get(_groupTag) != "":
 		var err error
@@ -471,17 +469,16 @@ func newResultGrouped(f reflect.StructField) (resultGrouped, error) {
 	optional, _ := isFieldOptional(f)
 	switch {
 	case g.Flatten && f.Type.Kind() != reflect.Slice:
-		return rg, errf("flatten can be applied to slices only",
-			"field %q (%v) is not a slice", f.Name, f.Type)
+		return rg, newErrInvalidInput(fmt.Sprintf(
+			"flatten can be applied to slices only: field %q (%v) is not a slice", f.Name, f.Type), nil)
 	case g.Soft:
-		return rg, errf("cannot use soft with result value groups",
-			"soft was used with group %q", rg.Group)
+		return rg, newErrInvalidInput(fmt.Sprintf(
+			"cannot use soft with result value groups: soft was used with group %q", rg.Group), nil)
 	case name != "":
-		return rg, errf(
-			"cannot use named values with value groups",
-			"name:%q provided with group:%q", name, rg.Group)
+		return rg, newErrInvalidInput(fmt.Sprintf(
+			"cannot use named values with value groups: name:%q provided with group:%q", name, rg.Group), nil)
 	case optional:
-		return rg, errors.New("value groups cannot be optional")
+		return rg, newErrInvalidInput("value groups cannot be optional", nil)
 	}
 	if g.Flatten {
 		rg.Type = f.Type.Elem()
diff --git a/visualize.go b/visualize.go
index c53c46a..d7ff324 100644
--- a/visualize.go
+++ b/visualize.go
@@ -21,6 +21,7 @@
 package dig
 
 import (
+	"errors"
 	"fmt"
 	"io"
 	"strconv"
@@ -62,27 +63,27 @@ func (o visualizeErrorOption) applyVisualizeOption(opt *visualizeOptions) {
 }
 
 func updateGraph(dg *dot.Graph, err error) error {
-	var errors []errVisualizer
+	var errs []errVisualizer
 	// Unwrap error to find the root cause.
 	for {
 		if ev, ok := err.(errVisualizer); ok {
-			errors = append(errors, ev)
+			errs = append(errs, ev)
 		}
-		e, ok := err.(causer)
-		if !ok {
+		e := errors.Unwrap(err)
+		if e == nil {
 			break
 		}
-		err = e.cause()
+		err = e
 	}
 
 	// If there are no errVisualizers included, we do not modify the graph.
-	if len(errors) == 0 {
+	if len(errs) == 0 {
 		return nil
 	}
 
 	// We iterate in reverse because the last element is the root cause.
-	for i := len(errors) - 1; i >= 0; i-- {
-		errors[i].updateGraph(dg)
+	for i := len(errs) - 1; i >= 0; i-- {
+		errs[i].updateGraph(dg)
 	}
 
 	// Remove non-error entries from the graph for readability.
@@ -156,11 +157,11 @@ func CanVisualizeError(err error) bool {
 		if _, ok := err.(errVisualizer); ok {
 			return true
 		}
-		e, ok := err.(causer)
-		if !ok {
+		e := errors.Unwrap(err)
+		if e == nil {
 			break
 		}
-		err = e.cause()
+		err = e
 	}
 
 	return false


Test patch:
diff --git a/dig_test.go b/dig_test.go
index 5b78b85..d609cd4 100644
--- a/dig_test.go
+++ b/dig_test.go
@@ -1525,7 +1525,7 @@ func TestProvideConstructorErrors(t *testing.T) {
 			{
 				desc:        "dig.Out",
 				constructor: func(out) io.Writer { return nil },
-				msg:         "dig_test.out embeds a dig.Out",
+				msg:         `dig_test.out embeds a dig.Out`,
 			},
 			{
 				desc:        "*dig.Out",
@@ -1547,8 +1547,7 @@ func TestProvideConstructorErrors(t *testing.T) {
 					`cannot provide function "go.uber.org/dig_test".TestProvideConstructorErrors\S+`,
 					`dig_test.go:\d+`, // file:line
 					`bad argument 1:`,
-					`cannot depend on result objects:`,
-					tt.msg)
+					`cannot depend on result objects: `+tt.msg)
 			})
 		}
 	})
@@ -1572,8 +1571,7 @@ func TestProvideConstructorErrors(t *testing.T) {
 			`cannot provide function "go.uber.org/dig_test".TestProvideConstructorErrors\S+`,
 			`dig_test.go:\d+`, // file:line
 			`bad result 1:`,
-			"cannot specify a name for result objects:",
-			"dig_test.out embeds dig.Out",
+			"cannot specify a name for result objects: dig_test.out embeds dig.Out",
 		)
 	})
 
@@ -1601,8 +1599,7 @@ func TestProvideConstructorErrors(t *testing.T) {
 			`cannot provide function "go.uber.org/dig_test".TestProvideConstructorErrors\S+`,
 			`dig_test.go:\d+`, // file:line
 			`bad field "Result1" of dig_test.Result2:`,
-			"cannot specify a name for result objects:",
-			"dig_test.Result1 embeds dig.Out",
+			"cannot specify a name for result objects: dig_test.Result1 embeds dig.Out",
 		)
 	})
 }
@@ -1909,8 +1906,7 @@ func TestCantProvideParameterObjects(t *testing.T) {
 			`cannot provide function "go.uber.org/dig_test".TestCantProvideParameterObjects\S+`,
 			`dig_test.go:\d+`, // file:line
 			"bad result 1:",
-			`cannot provide parameter objects:`,
-			`dig_test.Args embeds a dig.In`,
+			"cannot provide parameter objects: dig_test.Args embeds a dig.In",
 		)
 	})
 
@@ -1926,8 +1922,7 @@ func TestCantProvideParameterObjects(t *testing.T) {
 			`cannot provide function "go.uber.org/dig_test".TestCantProvideParameterObjects\S+`,
 			`dig_test.go:\d+`, // file:line
 			"bad result 1:",
-			`cannot provide parameter objects:`,
-			`\*dig_test.Args embeds a dig.In`,
+			`cannot provide parameter objects: \*dig_test.Args embeds a dig.In`,
 		)
 	})
 }
@@ -2203,6 +2198,18 @@ func testProvideCycleFails(t *testing.T, dryRun bool) {
 	})
 }
 
+func TestProvideErrNonCycle(t *testing.T) {
+	c := digtest.New(t)
+	type A struct{}
+	type B struct{}
+	newA := func() *A { return &A{} }
+
+	c.RequireProvide(newA)
+	err := c.Invoke(func(*B) {})
+	require.Error(t, err)
+	assert.False(t, dig.IsCycleDetected(err))
+}
+
 func TestIncompleteGraphIsOkay(t *testing.T) {
 	t.Parallel()
 
@@ -2407,8 +2414,7 @@ func testProvideFailures(t *testing.T, dryRun bool) {
 			`cannot provide function "go.uber.org/dig_test".testProvideFailures\S+`,
 			`dig_test.go:\d+`, // file:line
 			"bad result 1:",
-			`cannot return a pointer to a result object, use a value instead:`,
-			`\*dig_test.out is a pointer to a struct that embeds dig.Out`,
+			`cannot return a pointer to a result object, use a value instead: \*dig_test.out is a pointer to a struct that embeds dig.Out`,
 		)
 	})
 
@@ -2427,8 +2433,7 @@ func testProvideFailures(t *testing.T, dryRun bool) {
 			`cannot provide function "go.uber.org/dig_test".testProvideFailures\S+`,
 			`dig_test.go:\d+`, // file:line
 			"bad result 1:",
-			`cannot build a result object by embedding \*dig.Out, embed dig.Out instead:`,
-			`dig_test.out embeds \*dig.Out`,
+			`cannot build a result object by embedding \*dig.Out, embed dig.Out instead: dig_test.out embeds \*dig.Out`,
 		)
 	})
 
@@ -2905,8 +2910,7 @@ func testInvokeFailures(t *testing.T, dryRun bool) {
 		require.Error(t, err)
 		dig.AssertErrorMatches(t, err,
 			"bad argument 1:",
-			"cannot depend on a pointer to a parameter object, use a value instead:",
-			`\*dig_test.in is a pointer to a struct that embeds dig.In`,
+			`cannot depend on a pointer to a parameter object, use a value instead: \*dig_test.in is a pointer to a struct that embeds dig.In`,
 		)
 	})
 
@@ -2925,8 +2929,7 @@ func testInvokeFailures(t *testing.T, dryRun bool) {
 		require.Error(t, err)
 		dig.AssertErrorMatches(t, err,
 			"bad argument 1:",
-			"cannot depend on result objects:",
-			"dig_test.in embeds a dig.Out",
+			"cannot depend on result objects: dig_test.in embeds a dig.Out",
 		)
 	})
 
@@ -2942,8 +2945,7 @@ func testInvokeFailures(t *testing.T, dryRun bool) {
 		require.Error(t, err)
 		dig.AssertErrorMatches(t, err,
 			"bad argument 1:",
-			`cannot build a parameter object by embedding \*dig.In, embed dig.In instead:`,
-			`dig_test.in embeds \*dig.In`,
+			`cannot build a parameter object by embedding \*dig.In, embed dig.In instead: dig_test.in embeds \*dig.In`,
 		)
 	})
 
diff --git a/error_test.go b/error_test.go
index 7bf61d6..a658f9d 100644
--- a/error_test.go
+++ b/error_test.go
@@ -31,7 +31,6 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
 	"go.uber.org/dig/internal/digreflect"
 )
 
@@ -88,10 +87,6 @@ func AssertErrorMatches(t *testing.T, err error, msg string, msgs ...string) {
 	})
 }
 
-func Errf(msg string, args ...interface{}) error {
-	return errf(msg, args...)
-}
-
 // consumingFinder matches a string and returns the rest of the string *after*
 // the match.
 type consumingFinder interface {
@@ -117,9 +112,9 @@ func runFinders(original string, finders []consumingFinder) error {
 			// We won't use %q for the error message itself
 			// because we want it to be printed to the console as
 			// it would actually show.
-			return errf(`"%v" contains %v in the wrong place`, original, f)
+			return fmt.Errorf(`"%v" contains %v in the wrong place`, original, f)
 		}
-		return errf(`"%v" does not contain %v`, original, f)
+		return fmt.Errorf(`"%v" does not contain %v`, original, f)
 	}
 	return nil
 }
@@ -150,132 +145,140 @@ func (s stringFinder) Find(got string) (rest string, ok bool) {
 	return got[i+len(s):], true
 }
 
-func TestErrf(t *testing.T) {
-	type args = []interface{}
-
+func TestRootCause(t *testing.T) {
 	tests := []struct {
-		desc string
-		give error
-
-		wantV     string // output for %v
-		wantPlusV string // output for %+v
-
-		wantRootCause error
+		desc                    string
+		give                    error
+		wantAsDigError          bool
+		wantRootCause           error
+		wantRootCauseAsDigError bool
 	}{
 		{
-			desc:          "single unformatted error",
-			give:          errf("foo"),
-			wantV:         "foo",
-			wantPlusV:     "foo",
-			wantRootCause: errors.New("foo"),
+			desc:                    "random unformatted error",
+			give:                    errors.New("This non-Dig error is not formatted"),
+			wantAsDigError:          false,
+			wantRootCause:           errors.New("This non-Dig error is not formatted"),
+			wantRootCauseAsDigError: false,
 		},
 		{
-			desc:          "single formatted error",
-			give:          errf("foo %d %s", 42, "bar"),
-			wantV:         "foo 42 bar",
-			wantPlusV:     "foo 42 bar",
-			wantRootCause: errors.New("foo 42 bar"),
+			desc:                    "random formatted error",
+			give:                    fmt.Errorf("This non-Dig error is %v", "formatted"),
+			wantAsDigError:          false,
+			wantRootCause:           fmt.Errorf("This non-Dig error is %v", "formatted"),
+			wantRootCauseAsDigError: false,
 		},
 		{
-			desc:          "multiple unformatted errors",
-			give:          errf("foo", "bar", "baz"),
-			wantV:         "foo: bar: baz",
-			wantPlusV:     joinLines("foo:", "bar:", "baz"),
-			wantRootCause: errors.New("baz"),
+			desc:                    "simple errInvalidInput",
+			give:                    errInvalidInput{Message: "baz", Cause: nil},
+			wantAsDigError:          true,
+			wantRootCause:           errInvalidInput{Message: "baz", Cause: nil},
+			wantRootCauseAsDigError: true,
 		},
 		{
-			desc:          "multiple formatted errors",
-			give:          errf("foo %d", 42, "bar %s", "baz", "qux %q", "quux"),
-			wantV:         `foo 42: bar baz: qux "quux"`,
-			wantPlusV:     joinLines("foo 42:", "bar baz:", `qux "quux"`),
-			wantRootCause: errors.New(`qux "quux"`),
+			desc:                    "errInvalidInput wrapping errInvalidInput",
+			give:                    errInvalidInput{Message: "foo", Cause: errInvalidInput{Message: "bar", Cause: nil}},
+			wantAsDigError:          true,
+			wantRootCause:           errInvalidInput{Message: "bar", Cause: nil},
+			wantRootCauseAsDigError: true,
 		},
 		{
-			desc:          "single error",
-			give:          errf("foo", "bar", errors.New("great sadness")),
-			wantV:         "foo: bar: great sadness",
-			wantPlusV:     joinLines("foo:", "bar:", "great sadness"),
-			wantRootCause: errors.New("great sadness"),
+			desc:                    "errInvalidInput wrapping non-dig.Error",
+			give:                    errInvalidInput{Message: "foo", Cause: errors.New("bar")},
+			wantAsDigError:          true,
+			wantRootCause:           errors.New("bar"),
+			wantRootCauseAsDigError: false,
 		},
 		{
-			desc:          "multiple errors",
-			give:          errf("foo", "bar: %v", errors.New("baz"), errors.New("great sadness")),
-			wantV:         "foo: bar: baz: great sadness",
-			wantPlusV:     joinLines("foo:", "bar: baz:", "great sadness"),
-			wantRootCause: errors.New("great sadness"),
-		},
-		{
-			desc:          "escaped percent",
-			give:          errf("foo %% %v", "bar"),
-			wantV:         "foo % bar",
-			wantPlusV:     "foo % bar",
-			wantRootCause: errors.New("foo % bar"),
+			desc:                    "nil",
+			give:                    nil,
+			wantAsDigError:          false,
+			wantRootCause:           nil,
+			wantRootCauseAsDigError: false,
 		},
 	}
-
 	for _, tt := range tests {
 		t.Run(tt.desc, func(t *testing.T) {
-			err := tt.give
-			require.NotNil(t, err, "invalid test: must not be nil")
-
-			t.Run("Error", func(t *testing.T) {
-				assert.Equal(t, tt.wantV, err.Error())
-			})
-
-			t.Run("format with %+v", func(t *testing.T) {
-				assert.Equal(t, tt.wantPlusV, fmt.Sprintf("%+v", err))
-			})
-
-			t.Run("RootCause", func(t *testing.T) {
-				assert.Equal(t, tt.wantRootCause, RootCause(err))
-			})
+			var de Error
+			assert.Equal(t, tt.wantAsDigError, errors.As(tt.give, &de))
+			gotRootCause := RootCause(tt.give)
+			assert.Equal(t, gotRootCause, tt.wantRootCause)
+			assert.Equal(t, tt.wantRootCause, gotRootCause, "incorrect root cause")
+			assert.Equal(t, tt.wantRootCauseAsDigError, errors.As(gotRootCause, &de))
 		})
 	}
 }
 
-func TestErrfInvalid(t *testing.T) {
-	t.Run("nil panics", func(t *testing.T) {
-		assert.Panics(t, func() {
-			errf("foo", nil)
-		})
-	})
-
-	t.Run("too few argumetns", func(t *testing.T) {
-		assert.Panics(t, func() {
-			errf("foo %v")
-		})
-	})
-
-	t.Run("error before last", func(t *testing.T) {
-		assert.Panics(t, func() {
-			errf("foo", errors.New("bar"), "baz %v", 42)
-		})
-	})
+type MyNonDigError struct {
+	msg string
+}
 
-	t.Run("unknown type", func(t *testing.T) {
-		assert.Panics(t, func() {
-			errf("foo %v", 42, 43)
-		})
-	})
+func (e MyNonDigError) Error() string {
+	return e.msg
 }
 
-func TestNumFmtArgs(t *testing.T) {
+func TestRootCauseEndToEnd(t *testing.T) {
+
 	tests := []struct {
-		desc string
-		give string
-		want int
+		desc                    string
+		setup                   func(c *Container)
+		invoke                  interface{}
+		wantAsDigError          bool
+		wantRootCauseMessage    string
+		wantRootCauseAsDigError bool
 	}{
-		{"empty", "", 0},
-		{"none", "foo bar", 0},
-		{"some", "foo %v b %d ar", 2},
-		{"trailing", "foo %v 100%", 1},
-		{"escaped", "foo %v bar %% baz %d", 2},
+		{
+			desc: "error thrown in constructor",
+			setup: func(c *Container) {
+				assert.NoError(t, c.Provide(func() (string, error) {
+					return "hello", MyNonDigError{msg: "great sadness"}
+				}))
+			},
+			invoke: func(s string) {
+				fmt.Println(s)
+			},
+			wantAsDigError:          true,
+			wantRootCauseMessage:    "great sadness",
+			wantRootCauseAsDigError: false,
+		},
+		{
+			desc: "parameter not available in container",
+			setup: func(c *Container) {
+				assert.NoError(t, c.Provide(func() int { return 5 }))
+			},
+			invoke: func(s string) {
+				fmt.Println(s)
+			},
+			wantAsDigError:          true,
+			wantRootCauseMessage:    "missing type: string",
+			wantRootCauseAsDigError: true,
+		},
+		{
+			desc: "error in invoke",
+			setup: func(c *Container) {
+				assert.NoError(t, c.Provide(func() (string, error) {
+					return "hello", nil
+				}))
+			},
+			invoke: func(s string) error {
+				return errors.New("terrible unhappiness")
+			},
+			wantAsDigError:          false,
+			wantRootCauseMessage:    "terrible unhappiness",
+			wantRootCauseAsDigError: false,
+		},
 	}
 
 	for _, tt := range tests {
-		t.Run(tt.desc, func(t *testing.T) {
-			assert.Equal(t, tt.want, numFmtArgs(tt.give))
-		})
+		c := New()
+		tt.setup(c)
+		var de Error
+		err := c.Invoke(tt.invoke)
+		assert.Equal(t, tt.wantAsDigError, errors.As(err, &de),
+			fmt.Sprintf("expected errors.As() to return %v", tt.wantAsDigError))
+		rcErr := RootCause(err)
+		assert.Equal(t, tt.wantRootCauseMessage, rcErr.Error())
+		assert.Equal(t, tt.wantRootCauseAsDigError, errors.As(rcErr, &de),
+			fmt.Sprintf("expected errors.As() on the root cause to return %v", tt.wantRootCauseAsDigError))
 	}
 }
 
@@ -381,10 +384,10 @@ func TestErrorFormatting(t *testing.T) {
 		wantPlusV  string
 	}{
 		{
-			desc: "wrappedError/simple",
-			give: wrappedError{
-				msg: "something went wrong",
-				err: simpleErr,
+			desc: "wrapped error/simple",
+			give: errInvalidInput{
+				Message: "something went wrong",
+				Cause:   simpleErr,
 			},
 			wantString: "something went wrong: great sadness",
 			wantPlusV: joinLines(
@@ -393,10 +396,10 @@ func TestErrorFormatting(t *testing.T) {
 			),
 		},
 		{
-			desc: "wrappedError/rich",
-			give: wrappedError{
-				msg: "something went wrong",
-				err: richError,
+			desc: "wrapped error/rich",
+			give: errInvalidInput{
+				Message: "something went wrong",
+				Cause:   richError,
 			},
 			wantString: "something went wrong: great sadness",
 			wantPlusV: joinLines(
diff --git a/result_test.go b/result_test.go
index 3c0e51b..c19db20 100644
--- a/result_test.go
+++ b/result_test.go
@@ -32,12 +32,14 @@ import (
 
 func TestNewResultListErrors(t *testing.T) {
 	tests := []struct {
-		desc string
-		give interface{}
+		desc      string
+		give      interface{}
+		wantError string
 	}{
 		{
-			desc: "returns dig.In",
-			give: func() struct{ In } { panic("invalid") },
+			desc:      "returns dig.In",
+			give:      func() struct{ In } { panic("invalid") },
+			wantError: "struct { dig.In } embeds a dig.In",
 		},
 		{
 			desc: "returns dig.Out+dig.In",
@@ -47,6 +49,7 @@ func TestNewResultListErrors(t *testing.T) {
 			} {
 				panic("invalid")
 			},
+			wantError: "struct { dig.Out; dig.In } embeds a dig.In",
 		},
 	}
 
@@ -56,8 +59,7 @@ func TestNewResultListErrors(t *testing.T) {
 			require.Error(t, err)
 			AssertErrorMatches(t, err,
 				"bad result 1:",
-				"cannot provide parameter objects:",
-				"embeds a dig.In")
+				`cannot provide parameter objects: `+tt.wantError)
 		})
 	}
 }
diff --git a/visualize_int_test.go b/visualize_int_test.go
index 6d1b3ad..c94d5f7 100644
--- a/visualize_int_test.go
+++ b/visualize_int_test.go
@@ -21,6 +21,7 @@
 package dig
 
 import (
+	"errors"
 	"fmt"
 	"io"
 	"testing"
@@ -41,17 +42,13 @@ type nestedErr struct {
 	err error
 }
 
-var _ causer = nestedErr{}
+var _ digError = nestedErr{}
 
 func (e nestedErr) Error() string {
 	return fmt.Sprint(e)
 }
 
-func (e nestedErr) Format(w fmt.State, c rune) {
-	formatCauser(e, w, c)
-}
-
-func (e nestedErr) cause() error {
+func (e nestedErr) Unwrap() error {
 	return e.err
 }
 
@@ -59,6 +56,10 @@ func (e nestedErr) writeMessage(w io.Writer, _ string) {
 	io.WriteString(w, "oh no")
 }
 
+func (e nestedErr) Format(w fmt.State, c rune) {
+	formatError(e, w, c)
+}
+
 type visualizableErr struct{}
 
 func (err visualizableErr) Error() string             { return "great sadness" }
@@ -72,12 +73,12 @@ func TestCanVisualizeError(t *testing.T) {
 	}{
 		{
 			desc:         "unvisualizable error",
-			err:          errf("great sadness"),
+			err:          errors.New("great sadness"),
 			canVisualize: false,
 		},
 		{
 			desc:         "nested unvisualizable error",
-			err:          nestedErr{err: errf("great sadness")},
+			err:          nestedErr{err: errors.New("great sadness")},
 			canVisualize: false,
 		},
 		{
diff --git a/visualize_test.go b/visualize_test.go
index db6f20a..de5b8a8 100644
--- a/visualize_test.go
+++ b/visualize_test.go
@@ -512,7 +512,7 @@ func TestVisualize(t *testing.T) {
 		c.Provide(func(in1) out1 { return out1{} })
 		c.Provide(func(in2) t4 { return t4{} })
 		c.Provide(func() out2 { return out2{} })
-		c.Provide(func() (out3, error) { return out3{}, dig.Errf("great sadness") })
+		c.Provide(func() (out3, error) { return out3{}, errors.New("great sadness") })
 		err := c.Invoke(func(t4 t4) {})
 
 		dig.VerifyVisualization(t, "error", c.Container, dig.VisualizeError(err))
@@ -523,7 +523,7 @@ func TestVisualize(t *testing.T) {
 				c := digtest.New(t)
 				c.Provide(func(in1) out1 { return out1{} })
 				c.Provide(func(in2) t4 { return t4{} })
-				c.Provide(func() (out2, error) { return out2{}, dig.Errf("great sadness") })
+				c.Provide(func() (out2, error) { return out2{}, errors.New("great sadness") })
 				c.Provide(func() out3 { return out3{} })
 				err := c.Invoke(func(t4 t4) {})
 
@@ -533,7 +533,7 @@ func TestVisualize(t *testing.T) {
 			t.Run("if only the root node fails all node except for the root should be pruned", func(t *testing.T) {
 				c := digtest.New(t)
 				c.Provide(func(in1) out1 { return out1{} })
-				c.Provide(func(in2) (t4, error) { return t4{}, dig.Errf("great sadness") })
+				c.Provide(func(in2) (t4, error) { return t4{}, errors.New("great sadness") })
 				c.Provide(func() out2 { return out2{} })
 				c.Provide(func() out3 { return out3{} })
 				err := c.Invoke(func(t4 t4) {})