Repository: uber-go/dig
Pull request #257 (2019-12-18 22:33:09)

Print multi-line error messages with %+v
*Commits are individually reviewable.*

This adds a series of commits that add support for printing multi-line error
messages when `%+v` is used.

To do so, we implement [`fmt.Formatter`][1] on all relevant error types,
interpreting `%v` the old way and `%+v` as a signal to be prettier.

  [1]: https://golang.org/pkg/fmt/#Formatter

Implementation `fmt.Formatter` as-is for the error-chain types (those
implementing the `causer` interface) causes a fair amount of duplication so
this change also adds some convenience around it. Specifically, these types
now implement a means of printing just their error message to the output,
deferring to a shared implementation to print the chain of causes.

Finally, as tests that verify error messages all use the same helper
(`assertErrorMatches`), the helper was split into two subtests. One of the
subtests verifies the old behavior and the latter verifies the new `%+v`
behavior.

Refs T2587659

abhinav: Not yet ready for review.
codecov[bot]: # [Codecov](https://codecov.io/gh/uber-go/dig/pull/257?src=pr&el=h1) Report
> Merging [#257](https://codecov.io/gh/uber-go/dig/pull/257?src=pr&el=desc) into [abg/errf](https://codecov.io/gh/uber-go/dig/commit/b0e22b928e6408d12888d949abc6190fabcc4dd1?src=pr&el=desc) will **decrease** coverage by `0.32%`.
> The diff coverage is `95.45%`.

[![Impacted file tree graph](https://codecov.io/gh/uber-go/dig/pull/257/graphs/tree.svg?width=650&token=ARuH7Frwgw&height=150&src=pr)](https://codecov.io/gh/uber-go/dig/pull/257?src=pr&el=tree)

```diff
@@             Coverage Diff              @@
##           abg/errf     #257      +/-   ##
============================================
- Coverage     98.14%   97.82%   -0.33%     
============================================
  Files            12       12              
  Lines          1186     1239      +53     
============================================
+ Hits           1164     1212      +48     
- Misses           17       22       +5     
  Partials          5        5
```


| [Impacted Files](https://codecov.io/gh/uber-go/dig/pull/257?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [internal/digreflect/func.go](https://codecov.io/gh/uber-go/dig/pull/257/diff?src=pr&el=tree#diff-aW50ZXJuYWwvZGlncmVmbGVjdC9mdW5jLmdv) | `93.75% <100%> (+1.44%)` | :arrow_up: |
| [error.go](https://codecov.io/gh/uber-go/dig/pull/257/diff?src=pr&el=tree#diff-ZXJyb3IuZ28=) | `97.6% <95.06%> (-2.4%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/uber-go/dig/pull/257?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/uber-go/dig/pull/257?src=pr&el=footer). Last update [b0e22b9...ca73ea7](https://codecov.io/gh/uber-go/dig/pull/257?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

abhinav: Ready for review

Patch:
diff --git a/dig.go b/dig.go
index 2fcab16..6ba606e 100644
--- a/dig.go
+++ b/dig.go
@@ -695,7 +695,7 @@ func isFieldOptional(f reflect.StructField) (bool, error) {
 // Checks that all direct dependencies of the provided param are present in
 // the container. Returns an error if not.
 func shallowCheckDependencies(c containerStore, p param) error {
-	var missing errMissingManyTypes
+	var err errMissingTypes
 	var addMissingNodes []*dot.Param
 	walkParam(p, paramVisitorFunc(func(p param) bool {
 		ps, ok := p.(paramSingle)
@@ -704,15 +704,15 @@ func shallowCheckDependencies(c containerStore, p param) error {
 		}
 
 		if ns := c.getValueProviders(ps.Name, ps.Type); len(ns) == 0 && !ps.Optional {
-			missing = append(missing, newErrMissingType(c, key{name: ps.Name, t: ps.Type}))
+			err = append(err, newErrMissingTypes(c, key{name: ps.Name, t: ps.Type})...)
 			addMissingNodes = append(addMissingNodes, ps.DotParam()...)
 		}
 
 		return true
 	}))
 
-	if len(missing) > 0 {
-		return missing
+	if len(err) > 0 {
+		return err
 	}
 	return nil
 }
diff --git a/error.go b/error.go
index 72b23d9..a62dd56 100644
--- a/error.go
+++ b/error.go
@@ -21,9 +21,9 @@
 package dig
 
 import (
-	"bytes"
 	"errors"
 	"fmt"
+	"io"
 	"reflect"
 	"sort"
 
@@ -34,11 +34,44 @@ import (
 // Errors which know their underlying cause should implement this interface to
 // be compatible with RootCause.
 //
-// We use an unexported "cause" method instead of "Cause" because we don't
-// want dig-internal causes to be confused with the cause of the user-provided
-// errors. (For example, if the users are using github.com/pkg/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
+
+	// Writes the message or context for this error in the chain.
+	//
+	// verb is either %v or %+v.
+	writeMessage(w io.Writer, verb 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) {
+	multiline := w.Flag('+') && v == 'v'
+	verb := "%v"
+	if multiline {
+		verb = "%+v"
+	}
+
+	// "context: " or "context:\n"
+	c.writeMessage(w, verb)
+	io.WriteString(w, ":")
+	if multiline {
+		io.WriteString(w, "\n")
+	} else {
+		io.WriteString(w, " ")
+	}
+
+	fmt.Fprintf(w, verb, c.cause())
 }
 
 // RootCause returns the original error that caused the provided dig failure.
@@ -174,10 +207,19 @@ type wrappedError struct {
 	msg string
 }
 
-func (e wrappedError) cause() error { return e.err }
+var _ causer = wrappedError{}
 
-func (e wrappedError) Error() string {
-	return fmt.Sprintf("%v: %v", e.msg, e.err)
+func (e wrappedError) cause() error {
+	return e.err
+}
+
+func (e wrappedError) writeMessage(w io.Writer, _ string) {
+	io.WriteString(w, e.msg)
+}
+
+func (e wrappedError) Error() string { return fmt.Sprint(e) }
+func (e wrappedError) Format(w fmt.State, c rune) {
+	formatCauser(e, w, c)
 }
 
 // errProvide is returned when a constructor could not be Provided into the
@@ -187,10 +229,19 @@ type errProvide struct {
 	Reason error
 }
 
-func (e errProvide) cause() error { return e.Reason }
+var _ causer = errProvide{}
+
+func (e errProvide) cause() 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.Sprintf("function %v cannot be provided: %v", e.Func, e.Reason)
+func (e errProvide) Error() string { return fmt.Sprint(e) }
+func (e errProvide) Format(w fmt.State, c rune) {
+	formatCauser(e, w, c)
 }
 
 // errConstructorFailed is returned when a user-provided constructor failed
@@ -200,10 +251,19 @@ type errConstructorFailed struct {
 	Reason error
 }
 
-func (e errConstructorFailed) cause() error { return e.Reason }
+var _ causer = errConstructorFailed{}
+
+func (e errConstructorFailed) cause() 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.Sprintf("function %v returned a non-nil error: %v", e.Func, e.Reason)
+func (e errConstructorFailed) Error() string { return fmt.Sprint(e) }
+func (e errConstructorFailed) Format(w fmt.State, c rune) {
+	formatCauser(e, w, c)
 }
 
 // errArgumentsFailed is returned when a function could not be run because one
@@ -213,10 +273,19 @@ type errArgumentsFailed struct {
 	Reason error
 }
 
-func (e errArgumentsFailed) cause() error { return e.Reason }
+var _ causer = errArgumentsFailed{}
+
+func (e errArgumentsFailed) cause() 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.Sprintf("could not build arguments for function %v: %v", e.Func, e.Reason)
+func (e errArgumentsFailed) Error() string { return fmt.Sprint(e) }
+func (e errArgumentsFailed) Format(w fmt.State, c rune) {
+	formatCauser(e, w, c)
 }
 
 // errMissingDependencies is returned when the dependencies of a function are
@@ -226,10 +295,19 @@ type errMissingDependencies struct {
 	Reason error
 }
 
-func (e errMissingDependencies) cause() error { return e.Reason }
+var _ causer = errMissingDependencies{}
 
-func (e errMissingDependencies) Error() string {
-	return fmt.Sprintf("missing dependencies for function %v: %v", e.Func, e.Reason)
+func (e errMissingDependencies) cause() 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)
 }
 
 // errParamSingleFailed is returned when a paramSingle could not be built.
@@ -239,10 +317,19 @@ type errParamSingleFailed struct {
 	CtorID dot.CtorID
 }
 
-func (e errParamSingleFailed) cause() error { return e.Reason }
+var _ causer = errParamSingleFailed{}
 
-func (e errParamSingleFailed) Error() string {
-	return fmt.Sprintf("failed to build %v: %v", e.Key, e.Reason)
+func (e errParamSingleFailed) cause() 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)
 }
 
 func (e errParamSingleFailed) updateGraph(g *dot.Graph) {
@@ -264,31 +351,96 @@ type errParamGroupFailed struct {
 	CtorID dot.CtorID
 }
 
-func (e errParamGroupFailed) cause() error { return e.Reason }
+var _ causer = errParamGroupFailed{}
+
+func (e errParamGroupFailed) cause() 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.Sprintf("could not build value group %v: %v", e.Key, e.Reason)
+func (e errParamGroupFailed) Error() string { return fmt.Sprint(e) }
+func (e errParamGroupFailed) Format(w fmt.State, c rune) {
+	formatCauser(e, w, c)
 }
 
 func (e errParamGroupFailed) updateGraph(g *dot.Graph) {
 	g.FailGroupNodes(e.Key.group, e.Key.t, e.CtorID)
 }
 
-// errMissingType is returned when a single value that was expected in the
-// container was not available.
-type errMissingType struct {
-	Key key
+// missingType holds information about a type that was missing in the
+// container.
+type missingType struct {
+	Key key // item that was missing
 
 	// If non-empty, we will include suggestions for what the user may have
 	// meant.
 	suggestions []key
 }
 
-func newErrMissingType(c containerStore, k key) errMissingType {
+// Format prints a string representation of missingType.
+//
+// With %v, it prints a short representation ideal for an itemized list.
+//
+//   io.Writer
+//   io.Writer: did you mean *bytes.Buffer?
+//   io.Writer: did you mean *bytes.Buffer, or *os.File?
+//
+// With %+v, it prints a longer representation ideal for standalone output.
+//
+//   io.Writer: did you mean to Provide it?
+//   io.Writer: did you mean to use *bytes.Buffer?
+//   io.Writer: did you mean to use one of *bytes.Buffer, or *os.File?
+func (mt missingType) Format(w fmt.State, v rune) {
+	plusV := w.Flag('+') && v == 'v'
+
+	fmt.Fprint(w, mt.Key)
+	switch len(mt.suggestions) {
+	case 0:
+		if plusV {
+			io.WriteString(w, " (did you mean to Provide it?)")
+		}
+	case 1:
+		sug := mt.suggestions[0]
+		if plusV {
+			fmt.Fprintf(w, " (did you mean to use %v?)", sug)
+		} else {
+			fmt.Fprintf(w, " (did you mean %v?)", sug)
+		}
+	default:
+		if plusV {
+			io.WriteString(w, " (did you mean to use one of ")
+		} else {
+			io.WriteString(w, " (did you mean ")
+		}
+
+		lastIdx := len(mt.suggestions) - 1
+		for i, sug := range mt.suggestions {
+			if i > 0 {
+				io.WriteString(w, ", ")
+				if i == lastIdx {
+					io.WriteString(w, "or ")
+				}
+			}
+			fmt.Fprint(w, sug)
+		}
+		io.WriteString(w, "?)")
+	}
+}
+
+// errMissingType is returned when one or more values that were expected in
+// the container were not available.
+//
+// Multiple instances of this error may be merged together by appending them.
+type errMissingTypes []missingType // inv: len > 0
+
+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.
-
 	suggestions := []reflect.Type{reflect.PtrTo(k.t)}
+
 	if k.t.Kind() == reflect.Ptr {
 		// The user requested a pointer but maybe we have a value.
 		suggestions = append(suggestions, k.t.Elem())
@@ -317,98 +469,60 @@ func newErrMissingType(c containerStore, k key) errMissingType {
 	// suggestions.
 	sort.Sort(byTypeName(suggestions))
 
-	err := errMissingType{Key: k}
+	mt := missingType{Key: k}
 	for _, t := range suggestions {
 		if len(c.getValueProviders(k.name, t)) > 0 {
 			k.t = t
-			err.suggestions = append(err.suggestions, k)
+			mt.suggestions = append(mt.suggestions, k)
 		}
 	}
 
-	return err
+	return errMissingTypes{mt}
 }
 
-func (e errMissingType) Error() string {
-	// Sample messages:
-	//
-	//   type io.Reader is not in the container, did you mean to Provide it?
-	//   type io.Reader is not in the container, did you mean to use one of *bytes.Buffer, *MyBuffer
-	//   type bytes.Buffer is not in the container, did you mean to use *bytes.Buffer?
-	//   type *foo[name="bar"] is not in the container, did you mean to use foo[name="bar"]?
-
-	b := new(bytes.Buffer)
-
-	fmt.Fprintf(b, "type %v is not in the container", e.Key)
-	switch len(e.suggestions) {
-	case 0:
-		b.WriteString(", did you mean to Provide it?")
-	case 1:
-		fmt.Fprintf(b, ", did you mean to use %v?", e.suggestions[0])
-	default:
-		b.WriteString(", did you mean to use one of ")
-		for i, k := range e.suggestions {
-			if i > 0 {
-				b.WriteString(", ")
-				if i == len(e.suggestions)-1 {
-					b.WriteString("or ")
-				}
-			}
-			fmt.Fprint(b, k)
-		}
-		b.WriteString("?")
-	}
-
-	return b.String()
+func (e errMissingTypes) Error() string {
+	return fmt.Sprint(e)
 }
 
-// errMissingManyTypes combines multiple errMissingType errors.
-type errMissingManyTypes []errMissingType // length must be non-zero
+func (e errMissingTypes) Format(w fmt.State, v rune) {
+	multiline := w.Flag('+') && v == 'v'
 
-func (e errMissingManyTypes) Error() string {
 	if len(e) == 1 {
-		return e[0].Error()
+		io.WriteString(w, "missing type:")
+	} else {
+		io.WriteString(w, "missing types:")
 	}
 
-	b := new(bytes.Buffer)
+	if !multiline {
+		// With %v, we need a space between : since the error
+		// won't be on a new line.
+		io.WriteString(w, " ")
+	}
 
-	b.WriteString("the following types are not in the container: ")
-	for i, err := range e {
-		if i > 0 {
-			b.WriteString("; ")
+	for i, mt := range e {
+		if multiline {
+			io.WriteString(w, "\n\t- ")
+		} else if i > 0 {
+			io.WriteString(w, "; ")
 		}
-		fmt.Fprintf(b, "%v", err.Key)
-		switch len(err.suggestions) {
-		case 0:
-			// do nothing
-		case 1:
-			fmt.Fprintf(b, " (did you mean %v?)", err.suggestions[0])
-		default:
-			b.WriteString(" (did you mean ")
-			for i, k := range err.suggestions {
-				if i > 0 {
-					b.WriteString(", ")
-					if i == len(err.suggestions)-1 {
-						b.WriteString("or ")
-					}
-				}
-				fmt.Fprint(b, k)
-			}
-			b.WriteString("?)")
+
+		if multiline {
+			fmt.Fprintf(w, "%+v", mt)
+		} else {
+			fmt.Fprintf(w, "%v", mt)
 		}
 	}
-
-	return b.String()
 }
 
-func (e errMissingManyTypes) updateGraph(g *dot.Graph) {
+func (e errMissingTypes) updateGraph(g *dot.Graph) {
 	missing := make([]*dot.Result, len(e))
 
-	for i, err := range e {
+	for i, mt := range e {
 		missing[i] = &dot.Result{
 			Node: &dot.Node{
-				Name:  err.Key.name,
-				Group: err.Key.group,
-				Type:  err.Key.t,
+				Name:  mt.Key.name,
+				Group: mt.Key.group,
+				Type:  mt.Key.t,
 			},
 		}
 	}
diff --git a/internal/digreflect/func.go b/internal/digreflect/func.go
index 05758ce..a170b07 100644
--- a/internal/digreflect/func.go
+++ b/internal/digreflect/func.go
@@ -45,8 +45,21 @@ type Func struct {
 
 // String returns a string representation of the function.
 func (f *Func) String() string {
-	// "path/to/package".MyFunction (path/to/file.go:42)
-	return fmt.Sprintf("%q.%v (%v:%v)", f.Package, f.Name, f.File, f.Line)
+	return fmt.Sprint(f)
+}
+
+// Format implements fmt.Formatter for Func, printing a single-line
+// representation for %v and a multi-line one for %+v.
+func (f *Func) Format(w fmt.State, c rune) {
+	if w.Flag('+') && c == 'v' {
+		// "path/to/package".MyFunction
+		// 	path/to/file.go:42
+		fmt.Fprintf(w, "%q.%v", f.Package, f.Name)
+		fmt.Fprintf(w, "\n\t%v:%v", f.File, f.Line)
+	} else {
+		// "path/to/package".MyFunction (path/to/file.go:42)
+		fmt.Fprintf(w, "%q.%v (%v:%v)", f.Package, f.Name, f.File, f.Line)
+	}
 }
 
 // InspectFunc inspects and returns runtime information about the given
diff --git a/param.go b/param.go
index c639d8f..32933be 100644
--- a/param.go
+++ b/param.go
@@ -238,7 +238,7 @@ func (ps paramSingle) Build(c containerStore) (reflect.Value, error) {
 		if ps.Optional {
 			return reflect.Zero(ps.Type), nil
 		}
-		return _noValue, newErrMissingType(c, key{name: ps.Name, t: ps.Type})
+		return _noValue, newErrMissingTypes(c, key{name: ps.Name, t: ps.Type})
 	}
 
 	for _, n := range providers {


Test patch:
diff --git a/dig_go19_test.go b/dig_go19_test.go
index c70df4f..c52899f 100644
--- a/dig_go19_test.go
+++ b/dig_go19_test.go
@@ -60,7 +60,8 @@ func TestEndToEndSuccessWithAliases(t *testing.T) {
 		err := c.Provide(func() B { return B{} })
 		require.Error(t, err, "B should fail to provide")
 		assertErrorMatches(t, err,
-			`function "go.uber.org/dig".TestEndToEndSuccessWithAliases\S+ \(\S+:\d+\) cannot be provided:`,
+			`cannot provide function "go.uber.org/dig".TestEndToEndSuccessWithAliases\S+`,
+			`dig_go19_test.go:\d+`, // file:line
 			`cannot provide dig.A from \[0\]:`,
 			`already provided by "go.uber.org/dig".TestEndToEndSuccessWithAliases\S+`,
 		)
diff --git a/dig_test.go b/dig_test.go
index e56006c..74ce401 100644
--- a/dig_test.go
+++ b/dig_test.go
@@ -864,9 +864,10 @@ func TestEndToEndSuccess(t *testing.T) {
 		err := c.Invoke(func(B) {})
 		require.Error(t, err, "invoking with B param should error out")
 		assertErrorMatches(t, err,
-			`missing dependencies for function "go.uber.org/dig".TestEndToEndSuccess.func\S+ \(\S+/dig_test.go:\d+\):`,
-			"type dig.B is not in the container,",
-			"did you mean to Provide it?",
+			`missing dependencies for function "go.uber.org/dig".TestEndToEndSuccess.func\S+`,
+			`dig_test.go:\d+`, // file:line
+			"missing type:",
+			"dig.B",
 		)
 	})
 }
@@ -1210,7 +1211,8 @@ func TestGroups(t *testing.T) {
 		assertErrorMatches(t, err,
 			`could not build arguments for function "go.uber.org/dig".TestGroups`,
 			`could not build value group string\[group="x"\]:`,
-			`function "go.uber.org/dig".TestGroups\S+ \(\S+:\d+\) returned a non-nil error:`,
+			`received non-nil error from function "go.uber.org/dig".TestGroups\S+`,
+			`dig_test.go:\d+`, // file:line
 			"great sadness",
 		)
 		assert.Equal(t, gaveErr, RootCause(err))
@@ -1270,7 +1272,8 @@ func TestProvideConstructorErrors(t *testing.T) {
 				err := c.Provide(tt.constructor)
 				require.Error(t, err, "provide should fail")
 				assertErrorMatches(t, err,
-					`function "go.uber.org/dig".TestProvideConstructorErrors\S+ \(\S+:\d+\) cannot be provided:`,
+					`cannot provide function "go.uber.org/dig".TestProvideConstructorErrors\S+`,
+					`dig_test.go:\d+`, // file:line
 					`bad argument 1:`,
 					`cannot depend on result objects:`,
 					tt.msg)
@@ -1294,9 +1297,11 @@ func TestProvideConstructorErrors(t *testing.T) {
 		require.Error(t, err)
 
 		assertErrorMatches(t, err,
-			`function "go.uber.org/dig".TestProvideConstructorErrors\S+ \(\S+:\d+\) cannot be provided:`,
+			`cannot provide function "go.uber.org/dig".TestProvideConstructorErrors\S+`,
+			`dig_test.go:\d+`, // file:line
 			`bad result 1:`,
-			"cannot specify a name for result objects: dig.out embeds dig.Out",
+			"cannot specify a name for result objects:",
+			"dig.out embeds dig.Out",
 		)
 	})
 
@@ -1321,9 +1326,11 @@ func TestProvideConstructorErrors(t *testing.T) {
 		require.Error(t, err)
 
 		assertErrorMatches(t, err,
-			`function "go.uber.org/dig".TestProvideConstructorErrors\S+ \(\S+:\d+\) cannot be provided:`,
+			`cannot provide function "go.uber.org/dig".TestProvideConstructorErrors\S+`,
+			`dig_test.go:\d+`, // file:line
 			`bad field "Result1" of dig.Result2:`,
-			"cannot specify a name for result objects: dig.Result1 embeds dig.Out",
+			"cannot specify a name for result objects:",
+			"dig.Result1 embeds dig.Out",
 		)
 	})
 }
@@ -1347,9 +1354,11 @@ func TestProvideRespectsConstructorErrors(t *testing.T) {
 		var called bool
 		err := c.Invoke(func(b *bytes.Buffer) { called = true })
 		assertErrorMatches(t, err,
-			`could not build arguments for function "go.uber.org/dig".TestProvideRespectsConstructorErrors\S+ \(\S+:\d+\):`,
+			`could not build arguments for function "go.uber.org/dig".TestProvideRespectsConstructorErrors\S+`,
+			`dig_test.go:\d+`, // file:line
 			`failed to build \*bytes.Buffer:`,
-			`function "go.uber.org/dig".TestProvideRespectsConstructorErrors\S+ \(\S+:\d+\) returned a non-nil error:`,
+			`received non-nil error from function "go.uber.org/dig".TestProvideRespectsConstructorErrors\S+`,
+			`dig_test.go:\d+`, // file:line
 			`oh no`)
 		assert.False(t, called, "shouldn't call invoked function when deps aren't available")
 	})
@@ -1493,7 +1502,8 @@ func TestCantProvideParameterObjects(t *testing.T) {
 		})
 		require.Error(t, err, "provide should fail")
 		assertErrorMatches(t, err,
-			`function "go.uber.org/dig".TestCantProvideParameterObjects\S+ \(\S+:\d+\) cannot be provided:`,
+			`cannot provide function "go.uber.org/dig".TestCantProvideParameterObjects\S+`,
+			`dig_test.go:\d+`, // file:line
 			"bad result 1:",
 			`cannot provide parameter objects:`,
 			`dig.Args embeds a dig.In`,
@@ -1509,7 +1519,8 @@ func TestCantProvideParameterObjects(t *testing.T) {
 		err := c.Provide(func() (*Args, error) { return args, nil })
 		require.Error(t, err)
 		assertErrorMatches(t, err,
-			`function "go.uber.org/dig".TestCantProvideParameterObjects\S+ \(\S+:\d+\) cannot be provided:`,
+			`cannot provide function "go.uber.org/dig".TestCantProvideParameterObjects\S+`,
+			`dig_test.go:\d+`, // file:line
 			"bad result 1:",
 			`cannot provide parameter objects:`,
 			`\*dig.Args embeds a dig.In`,
@@ -1563,7 +1574,8 @@ func TestProvideCycleFails(t *testing.T) {
 		require.Error(t, err, "expected error when introducing cycle")
 		require.True(t, IsCycleDetected(err))
 		assertErrorMatches(t, err,
-			`function "go.uber.org/dig".TestProvideCycleFails.\S+ \(\S+:\d+\) cannot be provided:`,
+			`cannot provide function "go.uber.org/dig".TestProvideCycleFails.\S+`,
+			`dig_test.go:\d+`, // file:line
 			`this function introduces a cycle:`,
 			`\*dig.C provided by "go.uber.org/dig".TestProvideCycleFails\S+ \(\S+\)`,
 			`depends on \*dig.B provided by "go.uber.org/dig".TestProvideCycleFails.\S+ \(\S+\)`,
@@ -1609,7 +1621,8 @@ func TestProvideCycleFails(t *testing.T) {
 		require.Error(t, err, "expected error when introducing cycle")
 		require.True(t, IsCycleDetected(err))
 		assertErrorMatches(t, err,
-			`function "go.uber.org/dig".TestProvideCycleFails.\S+ \(\S+:\d+\) cannot be provided:`,
+			`cannot provide function "go.uber.org/dig".TestProvideCycleFails.\S+`,
+			`dig_test.go:\d+`, // file:line
 			`this function introduces a cycle:`,
 			`dig.C provided by "go.uber.org/dig".TestProvideCycleFails\S+ \(\S+\)`,
 			`depends on dig.B provided by "go.uber.org/dig".TestProvideCycleFails.\S+ \(\S+\)`,
@@ -1679,7 +1692,8 @@ func TestProvideCycleFails(t *testing.T) {
 		require.Error(t, err)
 		require.True(t, IsCycleDetected(err))
 		assertErrorMatches(t, err,
-			`function "go.uber.org/dig".TestProvideCycleFails.\S+ \(\S+:\d+\) cannot be provided:`,
+			`cannot provide function "go.uber.org/dig".TestProvideCycleFails.\S+`,
+			`dig_test.go:\d+`, // file:line
 			`this function introduces a cycle:`,
 			`\*dig.D provided by "go.uber.org/dig".TestProvideCycleFails\S+ \(\S+\)`,
 			`depends on int\[group="bar"\] provided by "go.uber.org/dig".TestProvideCycleFails.\S+ \(\S+\)`,
@@ -1819,7 +1833,8 @@ func TestProvideFailures(t *testing.T) {
 		})
 		require.Error(t, err, "provide must return error")
 		assertErrorMatches(t, err,
-			`function "go.uber.org/dig".TestProvideFailures\S+ \(\S+:\d+\) cannot be provided:`,
+			`cannot provide function "go.uber.org/dig".TestProvideFailures\S+`,
+			`dig_test.go:\d+`, // file:line
 			`cannot provide dig.A from \[0\].A2:`,
 			`already provided by \[0\].A1`,
 		)
@@ -1844,7 +1859,8 @@ func TestProvideFailures(t *testing.T) {
 		})
 		require.Error(t, err, "expected error on the second provide")
 		assertErrorMatches(t, err,
-			`function "go.uber.org/dig".TestProvideFailures\S+ \(\S+:\d+\) cannot be provided:`,
+			`cannot provide function "go.uber.org/dig".TestProvideFailures\S+`,
+			`dig_test.go:\d+`, // file:line
 			`cannot provide \*dig.A\[name="foo"\] from \[0\].A:`,
 			`already provided by "go.uber.org/dig".TestProvideFailures\S+`,
 		)
@@ -1863,7 +1879,8 @@ func TestProvideFailures(t *testing.T) {
 		err := c.Provide(func() out1 { return out1{a2: A{77}} })
 		require.Error(t, err)
 		assertErrorMatches(t, err,
-			`function "go.uber.org/dig".TestProvideFailures\S+ \(\S+:\d+\) cannot be provided:`,
+			`cannot provide function "go.uber.org/dig".TestProvideFailures\S+`,
+			`dig_test.go:\d+`, // file:line
 			"bad result 1:",
 			`bad field "a2" of dig.out1:`,
 			`unexported fields not allowed in dig.Out, did you mean to export "a2" \(dig.A\)\?`,
@@ -1880,7 +1897,8 @@ func TestProvideFailures(t *testing.T) {
 		err := c.Provide(func() *out { return &out{String: "foo"} })
 		require.Error(t, err)
 		assertErrorMatches(t, err,
-			`function "go.uber.org/dig".TestProvideFailures\S+ \(\S+:\d+\) cannot be provided:`,
+			`cannot provide function "go.uber.org/dig".TestProvideFailures\S+`,
+			`dig_test.go:\d+`, // file:line
 			"bad result 1:",
 			`cannot return a pointer to a result object, use a value instead:`,
 			`\*dig.out is a pointer to a struct that embeds dig.Out`,
@@ -1899,7 +1917,8 @@ func TestProvideFailures(t *testing.T) {
 		err := c.Provide(func() out { return out{String: "foo"} })
 		require.Error(t, err)
 		assertErrorMatches(t, err,
-			`function "go.uber.org/dig".TestProvideFailures\S+ \(\S+:\d+\) cannot be provided:`,
+			`cannot provide function "go.uber.org/dig".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.out embeds \*dig.Out`,
@@ -1930,8 +1949,10 @@ func TestInvokeFailures(t *testing.T) {
 		err := c.Invoke(func(*bytes.Buffer) {})
 		require.Error(t, err, "expected failure")
 		assertErrorMatches(t, err,
-			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures\S+ \(\S+\):`,
-			`type \*bytes.Buffer is not in the container, did you mean to Provide it\?`,
+			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures\S+`,
+			`dig_test.go:\d+`,
+			`missing type:`,
+			`\*bytes.Buffer`,
 		)
 	})
 
@@ -1953,8 +1974,10 @@ func TestInvokeFailures(t *testing.T) {
 
 		require.Error(t, err, "expected invoke error")
 		assertErrorMatches(t, err,
-			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures\S+ \(\S+\):`,
-			`type \*dig.type2 is not in the container, did you mean to Provide it\?`,
+			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures\S+`,
+			`dig_test.go:\d+`, // file:line
+			`missing type:`,
+			`\*dig.type2`,
 		)
 	})
 
@@ -1970,8 +1993,10 @@ func TestInvokeFailures(t *testing.T) {
 		})
 		require.Error(t, err, "invoke should fail")
 		assertErrorMatches(t, err,
-			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures.\S+ \(\S+\):`,
-			`type \*bytes.Buffer\[name="foo"\] is not in the container, did you mean to Provide it\?`,
+			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures.\S+`,
+			`dig_test.go:\d+`, // file:line
+			`missing type:`,
+			`\*bytes.Buffer\[name="foo"\]`,
 		)
 	})
 
@@ -1998,10 +2023,13 @@ func TestInvokeFailures(t *testing.T) {
 		})
 		require.Error(t, err, "invoke must fail")
 		assertErrorMatches(t, err,
-			`could not build arguments for function "go.uber.org/dig".TestInvokeFailures\S+ \(\S+:\d+\):`,
+			`could not build arguments for function "go.uber.org/dig".TestInvokeFailures\S+`,
+			`dig_test.go:\d+`, // file:line
 			`failed to build \*dig.type3:`,
-			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures.\S+ \(\S+\):`,
-			`type \*dig.type1 is not in the container, did you mean to Provide it\?`,
+			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures.\S+`,
+			`dig_test.go:\d+`, // file:line
+			`missing type:`,
+			`\*dig.type1`,
 		)
 		// We don't expect type2 to be mentioned in the list because it's
 		// optional
@@ -2028,12 +2056,14 @@ func TestInvokeFailures(t *testing.T) {
 
 		require.Error(t, err, "invoke must fail")
 		assertErrorMatches(t, err,
-			`could not build arguments for function "go.uber.org/dig".TestInvokeFailures\S+ \(\S+:\d+\):`,
+			`could not build arguments for function "go.uber.org/dig".TestInvokeFailures\S+`,
+			`dig_test.go:\d+`, // file:line
 			`failed to build dig.type3:`,
-			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures.\S+ \(\S+\):`,
-			`the following types are not in the container:`,
-			"dig.type1;",
-			`\*dig.type2 \(did you mean dig.type2\?\)`,
+			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures.\S+`,
+			`dig_test.go:\d+`, // file:line
+			`missing types:`,
+			"dig.type1",
+			`\*dig.type2 \(did you mean (to use )?dig.type2\?\)`,
 		)
 	})
 
@@ -2078,7 +2108,8 @@ func TestInvokeFailures(t *testing.T) {
 
 		require.Error(t, err, "expected provide error")
 		assertErrorMatches(t, err,
-			`function "go.uber.org/dig".TestInvokeFailures\S+ \(\S+:\d+\) cannot be provided:`,
+			`cannot provide function "go.uber.org/dig".TestInvokeFailures\S+`,
+			`dig_test.go:\d+`, // file:line
 			"bad argument 1:",
 			`bad field "Args" of dig.args:`,
 			`bad field "Buffer" of dig.nestedArgs:`,
@@ -2146,11 +2177,14 @@ func TestInvokeFailures(t *testing.T) {
 		})
 		require.Error(t, err, "expected invoke error")
 		assertErrorMatches(t, err,
-			`could not build arguments for function "go.uber.org/dig".TestInvokeFailures\S+ \(\S+:\d+\):`,
+			`could not build arguments for function "go.uber.org/dig".TestInvokeFailures\S+`,
+			`dig_test.go:\d+`, // file:line
 			`failed to build \*dig.dep:`,
-			`could not build arguments for function "go.uber.org/dig".TestInvokeFailures.\S+ \(\S+:\d+\):`,
+			`could not build arguments for function "go.uber.org/dig".TestInvokeFailures.\S+`,
+			`dig_test.go:\d+`, // file:line
 			`failed to build \*dig.failed:`,
-			`function "go.uber.org/dig".TestInvokeFailures.\S+ \(\S+:\d+\) returned a non-nil error:`,
+			`received non-nil error from function "go.uber.org/dig".TestInvokeFailures.\S+`,
+			`dig_test.go:\d+`, // file:line
 			`failed`,
 		)
 		assert.Equal(t, errFailed, RootCause(err), "root cause must match")
@@ -2188,8 +2222,10 @@ func TestInvokeFailures(t *testing.T) {
 		err := c.Invoke(func(param2) {})
 		require.Error(t, err, "provide should return error since cases don't match")
 		assertErrorMatches(t, err,
-			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures\S+ \(\S+:\d+\):`,
-			`type dig.A\[name="camelcase"\] is not in the container, did you mean to Provide it`)
+			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures\S+`,
+			`dig_test.go:\d+`, // file:line
+			`missing type:`,
+			`dig.A\[name="camelcase"\]`)
 	})
 
 	t.Run("in unexported member gets an error", func(t *testing.T) {
@@ -2223,7 +2259,8 @@ func TestInvokeFailures(t *testing.T) {
 		err := c.Provide(func(in) int { return 0 })
 		require.Error(t, err, "Provide must fail")
 		assertErrorMatches(t, err,
-			`function "go.uber.org/dig".TestInvokeFailures\S+ \(\S+:\d+\) cannot be provided:`,
+			`cannot provide function "go.uber.org/dig".TestInvokeFailures\S+`,
+			`dig_test.go:\d+`, // file:line
 			"bad argument 1:",
 			`bad field "foo" of dig.in:`,
 			`unexported fields not allowed in dig.In, did you mean to export "foo" \(string\)\?`,
@@ -2337,19 +2374,25 @@ func TestInvokeFailures(t *testing.T) {
 			name        string
 			provide     interface{}
 			invoke      interface{}
-			errContains string
+			errContains []string
 		}{
 			{
-				name:        "value missing, pointer present",
-				provide:     func() *A { return &A{} },
-				invoke:      func(A) {},
-				errContains: `type dig.A is not in the container, did you mean to use \*dig.A\?`,
+				name:    "value missing, pointer present",
+				provide: func() *A { return &A{} },
+				invoke:  func(A) {},
+				errContains: []string{
+					`missing type:`,
+					`dig.A \(did you mean (to use )?\*dig.A\?\)`,
+				},
 			},
 			{
-				name:        "pointer missing, value present",
-				provide:     func() A { return A{} },
-				invoke:      func(*A) {},
-				errContains: `type \*dig.A is not in the container, did you mean to use dig.A?`,
+				name:    "pointer missing, value present",
+				provide: func() A { return A{} },
+				invoke:  func(*A) {},
+				errContains: []string{
+					`missing type:`,
+					`\*dig.A \(did you mean (to use )?dig.A\?\)`,
+				},
 			},
 			{
 				name:    "named pointer missing, value present",
@@ -2360,7 +2403,10 @@ func TestInvokeFailures(t *testing.T) {
 					*A `name:"hello"`
 				}) {
 				},
-				errContains: `type \*dig.A\[name="hello"\] is not in the container, did you mean to use dig.A\[name="hello"\]?`,
+				errContains: []string{
+					`missing type:`,
+					`\*dig.A\[name="hello"\] \(did you mean (to use )?dig.A\[name="hello"\]\?\)`,
+				},
 			},
 		}
 
@@ -2371,10 +2417,13 @@ func TestInvokeFailures(t *testing.T) {
 
 				err := c.Invoke(tc.invoke)
 				require.Error(t, err)
+
+				lines := append([]string{
+					`dig_test.go:\d+`, // file:line
+				}, tc.errContains...)
 				assertErrorMatches(t, err,
-					`missing dependencies for function "go.uber.org/dig".TestInvokeFailures.\S+ \(\S+\):`,
-					tc.errContains,
-				)
+					`missing dependencies for function "go.uber.org/dig".TestInvokeFailures.\S+`,
+					lines...)
 			})
 		}
 	})
@@ -2387,8 +2436,10 @@ func TestInvokeFailures(t *testing.T) {
 		})
 		require.Error(t, err)
 		assertErrorMatches(t, err,
-			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures.\S+ \(\S+\):`,
-			`type io.Reader is not in the container, did you mean to use \*bytes.Reader\?`,
+			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures.\S+`,
+			`dig_test.go:\d+`, // file:line
+			`missing type:`,
+			`io.Reader \(did you mean (to use )?\*bytes.Reader\?\)`,
 		)
 	})
 
@@ -2403,8 +2454,10 @@ func TestInvokeFailures(t *testing.T) {
 		})
 		require.Error(t, err)
 		assertErrorMatches(t, err,
-			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures.\S+ \(\S+\):`,
-			`type io.Reader is not in the container, did you mean to use one of \*bytes.Buffer, or \*bytes.Reader\?`,
+			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures.\S+`,
+			`dig_test.go:\d+`, // file:line
+			`missing type:`,
+			`io.Reader \(did you mean (to use one of )?\*bytes.Buffer, or \*bytes.Reader\?\)`,
 		)
 	})
 
@@ -2419,10 +2472,10 @@ func TestInvokeFailures(t *testing.T) {
 		})
 		require.Error(t, err)
 		assertErrorMatches(t, err,
-			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures.\S+ \(\S+\):`,
-			`the following types are not in the container:`,
-			`io.Reader \(did you mean \*bytes.Buffer, or \*bytes.Reader\?\);`,
-			`io.Writer \(did you mean \*bytes.Buffer\?\)`,
+			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures.\S+`,
+			`dig_test.go:\d+`, // file:line
+			`missing types:`,
+			`io.Writer \(did you mean (to use )?\*bytes.Buffer\?\)`,
 		)
 	})
 
@@ -2436,8 +2489,10 @@ func TestInvokeFailures(t *testing.T) {
 
 		require.Error(t, err)
 		assertErrorMatches(t, err,
-			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures.\S+ \(\S+\):`,
-			`type \*bytes.Buffer is not in the container, did you mean to use io.Writer\?`,
+			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures.\S+`,
+			`dig_test.go:\d+`, // file:line
+			`missing type:`,
+			`\*bytes.Buffer \(did you mean (to use )?io.Writer\?\)`,
 		)
 	})
 
@@ -2453,8 +2508,10 @@ func TestInvokeFailures(t *testing.T) {
 
 		require.Error(t, err)
 		assertErrorMatches(t, err,
-			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures.\S+ \(\S+\):`,
-			`type \*bytes.Buffer is not in the container, did you mean to use one of io.Reader, or io.Writer\?`,
+			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures.\S+`,
+			`dig_test.go:\d+`, // file:line
+			`missing type:`,
+			`\*bytes.Buffer \(did you mean (to use one of )?io.Reader, or io.Writer\?\)`,
 		)
 	})
 
@@ -2471,7 +2528,8 @@ func TestInvokeFailures(t *testing.T) {
 
 		require.Error(t, err, "expected Invoke error")
 		assertErrorMatches(t, err,
-			`function "go.uber.org/dig".TestInvokeFailures.func\S+ \(\S+\) returned a non-nil error:`,
+			`received non-nil error from function "go.uber.org/dig".TestInvokeFailures.func\S+`,
+			`dig_test.go:\d+`, // file:line
 			"great sadness",
 		)
 		assert.Equal(t, errors.New("great sadness"), RootCause(err))
@@ -2499,7 +2557,8 @@ func TestInvokeFailures(t *testing.T) {
 			"failed to build dig.B",
 			`could not build arguments for function "go.uber.org/dig".TestInvokeFailures\S+`,
 			"failed to build dig.A",
-			`function "go.uber.org/dig".TestInvokeFailures.func\S+ \(\S+\) returned a non-nil error:`,
+			`received non-nil error from function "go.uber.org/dig".TestInvokeFailures.func\S+`,
+			`dig_test.go:\d+`, // file:line
 			"great sadness",
 		)
 		assert.Equal(t, errors.New("great sadness"), RootCause(err))
@@ -2526,7 +2585,8 @@ func TestInvokeFailures(t *testing.T) {
 		assertErrorMatches(t, err,
 			`could not build arguments for function "go.uber.org/dig".TestInvokeFailures.func\S+`,
 			"failed to build dig.A:",
-			`function "go.uber.org/dig".TestInvokeFailures.func\S+ \(\S+\) returned a non-nil error:`,
+			`received non-nil error from function "go.uber.org/dig".TestInvokeFailures.func\S+`,
+			`dig_test.go:\d+`, // file:line
 			"great sadness",
 		)
 		assert.Equal(t, errors.New("great sadness"), RootCause(err))
@@ -2556,11 +2616,13 @@ func TestInvokeFailures(t *testing.T) {
 
 		require.Error(t, err, "expected Invoke error")
 		assertErrorMatches(t, err,
-			`could not build arguments for function "go.uber.org/dig".TestInvokeFailures.func\S+ \(\S+:\d+\):`,
+			`could not build arguments for function "go.uber.org/dig".TestInvokeFailures.func\S+`,
+			`dig_test.go:\d+`, // file:line
 			"failed to build dig.B:",
 			`could not build arguments for function "go.uber.org/dig".TestInvokeFailures.func\S+`,
 			"failed to build dig.A:",
-			`function "go.uber.org/dig".TestInvokeFailures.func\S+ \(\S+\) returned a non-nil error:`,
+			`received non-nil error from function "go.uber.org/dig".TestInvokeFailures.func\S+`,
+			`dig_test.go:\d+`, // file:line
 			"great sadness",
 		)
 		assert.Equal(t, errors.New("great sadness"), RootCause(err))
@@ -2594,10 +2656,13 @@ func TestInvokeFailures(t *testing.T) {
 		})
 		require.Error(t, err, "expected failure")
 		assertErrorMatches(t, err,
-			`could not build arguments for function "go.uber.org/dig".TestInvokeFailures.\S+ \(\S+:\d+\):`,
+			`could not build arguments for function "go.uber.org/dig".TestInvokeFailures.\S+`,
+			`dig_test.go:\d+`, // file:line
 			`could not build value group dig.B\[group="b"\]:`,
-			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures.\S+ \(\S+:\d+\):`,
-			"type dig.A is not in the container, did you mean to Provide it?",
+			`missing dependencies for function "go.uber.org/dig".TestInvokeFailures.\S+`,
+			`dig_test.go:\d+`, // file:line
+			"missing type:",
+			"dig.A",
 		)
 	})
 }
diff --git a/error_test.go b/error_test.go
index 21ee484..469c98d 100644
--- a/error_test.go
+++ b/error_test.go
@@ -22,6 +22,9 @@ package dig
 
 import (
 	"errors"
+	"fmt"
+	"io"
+	"reflect"
 	"regexp"
 	"strconv"
 	"strings"
@@ -29,6 +32,7 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"go.uber.org/dig/internal/digreflect"
 )
 
 // assertErrorMatches matches error messages against the provided list of
@@ -45,7 +49,7 @@ import (
 // Because "bar" is not after "baz" in the error message.
 //
 // Messages will be treated as regular expressions.
-func assertErrorMatches(t testing.TB, err error, msg string, msgs ...string) {
+func assertErrorMatches(t *testing.T, err error, msg string, msgs ...string) {
 	// We have one positional argument in addition to the variadic argument to
 	// ensure that there's at least one string to match against.
 	if err == nil {
@@ -62,23 +66,24 @@ func assertErrorMatches(t testing.TB, err error, msg string, msgs ...string) {
 		}
 	}
 
-	original := err.Error()
-	remaining := original
-	for _, f := range finders {
-		if newRemaining, ok := f.Find(remaining); ok {
-			remaining = newRemaining
-			continue
-		}
+	t.Run("single line", func(t *testing.T) {
+		original := err.Error()
+		assert.NoError(t, runFinders(original, finders))
+	})
 
-		// Match not found. Check if the order was wrong.
-		if _, ok := f.Find(original); ok {
-			// 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.
-			t.Errorf(`"%v" contains %v in the wrong place`, original, f)
-		} else {
-			t.Errorf(`"%v" does not contain %v`, original, f)
+	// Intersperse "\n" finders between each message for the "%+v" check.
+	plusFinders := make([]consumingFinder, 0, len(finders)*2-1)
+	for i, f := range finders {
+		if i > 0 {
+			plusFinders = append(plusFinders, stringFinder("\n"))
 		}
+		plusFinders = append(plusFinders, f)
 	}
+
+	t.Run("multi line", func(t *testing.T) {
+		original := fmt.Sprintf("%+v", err)
+		assert.NoError(t, runFinders(original, plusFinders))
+	})
 }
 
 // consumingFinder matches a string and returns the rest of the string *after*
@@ -93,6 +98,26 @@ type consumingFinder interface {
 	Find(got string) (rest string, ok bool)
 }
 
+func runFinders(original string, finders []consumingFinder) error {
+	remaining := original
+	for _, f := range finders {
+		if newRemaining, ok := f.Find(remaining); ok {
+			remaining = newRemaining
+			continue
+		}
+
+		// Match not found. Check if the order was wrong.
+		if _, ok := f.Find(original); ok {
+			// 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 errf(`"%v" does not contain %v`, original, f)
+	}
+	return nil
+}
+
 type regexpFinder struct{ r *regexp.Regexp }
 
 func (r regexpFinder) String() string {
@@ -126,49 +151,58 @@ func TestErrf(t *testing.T) {
 		desc string
 		give error
 
-		wantMsg       string
+		wantV     string // output for %v
+		wantPlusV string // output for %+v
+
 		wantRootCause error
 	}{
 		{
 			desc:          "single unformatted error",
 			give:          errf("foo"),
-			wantMsg:       "foo",
+			wantV:         "foo",
+			wantPlusV:     "foo",
 			wantRootCause: errors.New("foo"),
 		},
 		{
 			desc:          "single formatted error",
 			give:          errf("foo %d %s", 42, "bar"),
-			wantMsg:       "foo 42 bar",
+			wantV:         "foo 42 bar",
+			wantPlusV:     "foo 42 bar",
 			wantRootCause: errors.New("foo 42 bar"),
 		},
 		{
 			desc:          "multiple unformatted errors",
 			give:          errf("foo", "bar", "baz"),
-			wantMsg:       "foo: bar: baz",
+			wantV:         "foo: bar: baz",
+			wantPlusV:     joinLines("foo:", "bar:", "baz"),
 			wantRootCause: errors.New("baz"),
 		},
 		{
 			desc:          "multiple formatted errors",
 			give:          errf("foo %d", 42, "bar %s", "baz", "qux %q", "quux"),
-			wantMsg:       `foo 42: bar baz: qux "quux"`,
+			wantV:         `foo 42: bar baz: qux "quux"`,
+			wantPlusV:     joinLines("foo 42:", "bar baz:", `qux "quux"`),
 			wantRootCause: errors.New(`qux "quux"`),
 		},
 		{
 			desc:          "single error",
 			give:          errf("foo", "bar", errors.New("great sadness")),
-			wantMsg:       "foo: bar: great sadness",
+			wantV:         "foo: bar: great sadness",
+			wantPlusV:     joinLines("foo:", "bar:", "great sadness"),
 			wantRootCause: errors.New("great sadness"),
 		},
 		{
 			desc:          "multiple errors",
 			give:          errf("foo", "bar: %v", errors.New("baz"), errors.New("great sadness")),
-			wantMsg:       "foo: bar: baz: 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"),
-			wantMsg:       "foo % bar",
+			wantV:         "foo % bar",
+			wantPlusV:     "foo % bar",
 			wantRootCause: errors.New("foo % bar"),
 		},
 	}
@@ -179,7 +213,11 @@ func TestErrf(t *testing.T) {
 			require.NotNil(t, err, "invalid test: must not be nil")
 
 			t.Run("Error", func(t *testing.T) {
-				assert.Equal(t, tt.wantMsg, err.Error())
+				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) {
@@ -234,3 +272,248 @@ func TestNumFmtArgs(t *testing.T) {
 		})
 	}
 }
+
+func joinLines(ls ...string) string { return strings.Join(ls, "\n") }
+
+// Simple error fake that provides control of %v and %+v representations.
+type errFormatted struct {
+	v     string //  output for %v
+	plusV string // output for %+v
+}
+
+var (
+	_ error         = errFormatted{}
+	_ fmt.Formatter = errFormatted{}
+)
+
+func (e errFormatted) Error() string { return e.v }
+
+func (e errFormatted) Format(w fmt.State, c rune) {
+	if w.Flag('+') && c == 'v' {
+		io.WriteString(w, e.plusV)
+	} else {
+		io.WriteString(w, e.v)
+	}
+}
+
+func TestMissingTypeFormatting(t *testing.T) {
+	type type1 struct{}
+	type someInterface interface{ stuff() }
+
+	tests := []struct {
+		desc      string
+		give      missingType
+		wantV     string
+		wantPlusV string
+	}{
+		{
+			desc: "no suggestions",
+			give: missingType{
+				Key: key{t: reflect.TypeOf(type1{})},
+			},
+			wantV:     "dig.type1",
+			wantPlusV: "dig.type1 (did you mean to Provide it?)",
+		},
+		{
+			desc: "one suggestion",
+			give: missingType{
+				Key: key{t: reflect.TypeOf(type1{})},
+				suggestions: []key{
+					{t: reflect.TypeOf(&type1{})},
+				},
+			},
+			wantV:     "dig.type1 (did you mean *dig.type1?)",
+			wantPlusV: "dig.type1 (did you mean to use *dig.type1?)",
+		},
+		{
+			desc: "many suggestions",
+			give: missingType{
+				Key: key{t: reflect.TypeOf(type1{})},
+				suggestions: []key{
+					{t: reflect.TypeOf(&type1{})},
+					{t: reflect.TypeOf(new(someInterface)).Elem()},
+				},
+			},
+			wantV:     "dig.type1 (did you mean *dig.type1, or dig.someInterface?)",
+			wantPlusV: "dig.type1 (did you mean to use one of *dig.type1, or dig.someInterface?)",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
+			assert.Equal(t, tt.wantV, fmt.Sprint(tt.give), "%v did not match")
+			assert.Equal(t, tt.wantPlusV, fmt.Sprintf("%+v", tt.give), "%+v did not match")
+		})
+	}
+}
+
+func TestErrorFormatting(t *testing.T) {
+	type someType struct{}
+	type anotherType struct{}
+
+	simpleErr := errors.New("great sadness")
+	richError := errFormatted{
+		v: "great sadness",
+		plusV: joinLines(
+			"sadness so great",
+			"it needs multiple",
+			"lines",
+		),
+	}
+
+	someFunc := &digreflect.Func{
+		Package: "foo",
+		Name:    "Bar",
+		File:    "foo/bar.go",
+		Line:    42,
+	}
+
+	tests := []struct {
+		desc       string
+		give       error
+		wantString string
+		wantPlusV  string
+	}{
+		{
+			desc: "wrappedError/simple",
+			give: wrappedError{
+				msg: "something went wrong",
+				err: simpleErr,
+			},
+			wantString: "something went wrong: great sadness",
+			wantPlusV: joinLines(
+				"something went wrong:",
+				"great sadness",
+			),
+		},
+		{
+			desc: "wrappedError/rich",
+			give: wrappedError{
+				msg: "something went wrong",
+				err: richError,
+			},
+			wantString: "something went wrong: great sadness",
+			wantPlusV: joinLines(
+				"something went wrong:",
+				"sadness so great",
+				"it needs multiple",
+				"lines",
+			),
+		},
+		{
+			desc: "errProvide",
+			give: errProvide{
+				Func:   someFunc,
+				Reason: simpleErr,
+			},
+			wantString: `cannot provide function "foo".Bar (foo/bar.go:42): great sadness`,
+			wantPlusV: joinLines(
+				`cannot provide function "foo".Bar`,
+				"	foo/bar.go:42:",
+				"great sadness",
+			),
+		},
+		{
+			desc: "errConstructorFailed",
+			give: errConstructorFailed{
+				Func:   someFunc,
+				Reason: richError,
+			},
+			wantString: `received non-nil error from function "foo".Bar (foo/bar.go:42): great sadness`,
+			wantPlusV: joinLines(
+				`received non-nil error from function "foo".Bar`,
+				"	foo/bar.go:42:",
+				"sadness so great",
+				"it needs multiple",
+				"lines",
+			),
+		},
+		{
+			desc: "errArgumentsFailed",
+			give: errArgumentsFailed{
+				Func:   someFunc,
+				Reason: simpleErr,
+			},
+			wantString: `could not build arguments for function "foo".Bar (foo/bar.go:42): great sadness`,
+			wantPlusV: joinLines(
+				`could not build arguments for function "foo".Bar`,
+				"	foo/bar.go:42:",
+				"great sadness",
+			),
+		},
+		{
+			desc: "errMissingDependencies",
+			give: errMissingDependencies{
+				Func:   someFunc,
+				Reason: richError,
+			},
+			wantString: `missing dependencies for function "foo".Bar (foo/bar.go:42): great sadness`,
+			wantPlusV: joinLines(
+				`missing dependencies for function "foo".Bar`,
+				"	foo/bar.go:42:",
+				"sadness so great",
+				"it needs multiple",
+				"lines",
+			),
+		},
+		{
+			desc: "errParamSingleFailed",
+			give: errParamSingleFailed{
+				Key:    key{t: reflect.TypeOf(someType{})},
+				Reason: richError,
+			},
+			wantString: `failed to build dig.someType: great sadness`,
+			wantPlusV: joinLines(
+				`failed to build dig.someType:`,
+				"sadness so great",
+				"it needs multiple",
+				"lines",
+			),
+		},
+		{
+			desc: "errParamGroupFailed",
+			give: errParamGroupFailed{
+				Key:    key{t: reflect.TypeOf(someType{}), group: "items"},
+				Reason: richError,
+			},
+			wantString: `could not build value group dig.someType[group="items"]: great sadness`,
+			wantPlusV: joinLines(
+				`could not build value group dig.someType[group="items"]:`,
+				"sadness so great",
+				"it needs multiple",
+				"lines",
+			),
+		},
+		{
+			desc: "errMissingTypes/single",
+			give: errMissingTypes{
+				{Key: key{t: reflect.TypeOf(someType{})}},
+			},
+			wantString: "missing type: dig.someType",
+			wantPlusV: joinLines(
+				"missing type:",
+				"	- dig.someType (did you mean to Provide it?)",
+			),
+		},
+		{
+			desc: "errMissingTypes/multiple",
+			give: errMissingTypes{
+				{Key: key{t: reflect.TypeOf(someType{})}},
+				{Key: key{t: reflect.TypeOf(&anotherType{})}},
+			},
+			wantString: "missing types: dig.someType; *dig.anotherType",
+			wantPlusV: joinLines(
+				"missing types:",
+				"	- dig.someType (did you mean to Provide it?)",
+				"	- *dig.anotherType (did you mean to Provide it?)",
+			),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
+			assert.Equal(t, tt.wantString, tt.give.Error(), "%v did not match")
+			assert.Equal(t, tt.wantPlusV, fmt.Sprintf("%+v", tt.give), "%+v did not match")
+		})
+	}
+}
diff --git a/graph_test.go b/graph_test.go
index 96facea..1c1d4c1 100644
--- a/graph_test.go
+++ b/graph_test.go
@@ -21,6 +21,8 @@
 package dig
 
 import (
+	"fmt"
+	"io"
 	"reflect"
 	"testing"
 
@@ -554,8 +556,23 @@ type nestedErr struct {
 	err error
 }
 
-func (err nestedErr) Error() string { return "oh no" }
-func (err nestedErr) cause() error  { return err.err }
+var _ causer = 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 {
+	return e.err
+}
+
+func (e nestedErr) writeMessage(w io.Writer, _ string) {
+	io.WriteString(w, "oh no")
+}
 
 func TestCanVisualizeError(t *testing.T) {
 	tests := []struct {
diff --git a/internal/digreflect/func_test.go b/internal/digreflect/func_test.go
index aeba7dc..ffa3c59 100644
--- a/internal/digreflect/func_test.go
+++ b/internal/digreflect/func_test.go
@@ -21,6 +21,7 @@
 package digreflect
 
 import (
+	"fmt"
 	"strings"
 	"testing"
 
@@ -55,7 +56,6 @@ func TestInspectFunc(t *testing.T) {
 		// We don't match the exact file name because $GOPATH can be anywhere
 		// on someone's system. Instead we'll match the suffix.
 		wantFileSuffix string
-		wantStringLike string
 	}{
 		{
 			desc:           "exported function",
@@ -123,13 +123,36 @@ func TestInspectFunc(t *testing.T) {
 
 			assert.True(t, strings.HasSuffix(f.File, tt.wantFileSuffix),
 				"file path %q does not end with src/%v", f.File, tt.wantFileSuffix)
-			assert.Contains(t, f.String(), tt.wantFileSuffix, "file path not in String output")
 		})
 	}
 }
 
-func TestSplitFuncEmptyString(t *testing.T) {
-	pname, fname := splitFuncName("")
-	assert.Empty(t, pname, "package name must be empty")
-	assert.Empty(t, fname, "function name must be empty")
+func TestSplitFunc(t *testing.T) {
+	t.Run("empty string", func(t *testing.T) {
+		pname, fname := splitFuncName("")
+		assert.Empty(t, pname, "package name must be empty")
+		assert.Empty(t, fname, "function name must be empty")
+	})
+
+	t.Run("vendored dependency", func(t *testing.T) {
+		pname, fname := splitFuncName("go.uber.orgc/dig/vendor/example.com/foo/bar.Baz")
+		assert.Equal(t, "example.com/foo/bar", pname)
+		assert.Equal(t, "Baz", fname)
+	})
+}
+
+func TestFuncFormatting(t *testing.T) {
+	f := Func{
+		Package: "foo/bar/baz",
+		Name:    "Qux",
+		File:    "src/foo/bar/baz/qux.go",
+		Line:    42,
+	}
+
+	assert.Equal(t,
+		`"foo/bar/baz".Qux (src/foo/bar/baz/qux.go:42)`,
+		f.String(), "%v did not match")
+
+	assert.Equal(t, `"foo/bar/baz".Qux
+	src/foo/bar/baz/qux.go:42`, fmt.Sprintf("%+v", &f), "%v did not match")
 }