Repository: goadesign/goa
Pull request #3063 (2022-05-03 02:29:02)

Wrap original error by goa.ServiceError 
I tried to implement #3058.

raphael: This looks good, a few questions:
* What happens for errors that do not have the `err` field initialized (e.g. all the validation errors)? `Unwrap` would return nil so `As` and `Is` would always fail? Maybe that's OK.
* Should `ServiceError` implement `As` and `Is` to make it more consistent? (use `err` if present otherwise compare the error names if the target is also a `ServiceError`). This is extending a bit the intent of this change but it seems to make sense.
* Should `newError` set the `err` field? Maybe not unless it helps with implementing `As` and `Is`.
tchssk: Thank you for your opinion.

I added `As`/`Is` to `ServiceError`. I think `newError` doesn't need to set `err`. It's a shorthand only used by error create functions and the functions have no `error` as argument.
raphael: Makes sense! A couple more questions:

* It seems `As` doesn't set the target when it's a `ServiceError`? seems like it should initialize the fields in this case? Also I believe the target might be `**ServiceError`, from the docs (emphasis mine):

>An error matches target if the error's concrete value is assignable to the value **pointed** to by target, or if the error has a method As(interface{}) bool such that As(target) returns true. In the latter case, the As method is responsible for setting target.

So the usage would be:

```go
res, err := client.SomeMethod(ctx, payload)
se := &ServiceError{name: "fault"}
if errors.As(err, &se) {
    log.Errorf(ctx, "faulted with error %s: %s", se.ID, se.Message)
}
```

`As` would have set the `ID` and `Message` fields of `se` (and the other fields of `ServiceError` as well).

* Should `Unwrap` be removed now that `As` and `Is` are implemented? If not should it check whether `err` is nil and if so return itself (the `ServiceError`) instead of `nil`?

Thank you!
tchssk: Sorry for late reply.
I reverted As/Is. Would you merge this once?

Patch:
diff --git a/Makefile b/Makefile
index 0be79d51..70ede26c 100644
--- a/Makefile
+++ b/Makefile
@@ -14,7 +14,7 @@
 #
 MAJOR=3
 MINOR=7
-BUILD=10
+BUILD=11
 
 GOOS=$(shell go env GOOS)
 GO_FILES=$(shell find . -type f -name '*.go')
diff --git a/README.md b/README.md
index 5eb4e465..857c4d67 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
 ## Overview
 
 [![Build Status](https://github.com/goadesign/goa/workflows/build/badge.svg?branch=v3&event=push)](https://github.com/goadesign/goa/actions?query=branch%3Av3+event%3Apush)
-[![GoDev](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/goa.design/goa/v3@v3.7.10/dsl?tab=doc)
+[![GoDev](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/goa.design/goa/v3@v3.7.11/dsl?tab=doc)
 [![Go Report Card](https://goreportcard.com/badge/goadesign/goa)](https://goreportcard.com/report/goadesign/goa)
 [![Slack](https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=red)](https://gophers.slack.com/messages/goa)
 [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/goadesign.svg?style=social&label=Follow%20%40goadesign)](https://twitter.com/goadesign)
@@ -49,7 +49,7 @@ go install goa.design/goa/v3/cmd/goa@v3
 
 >Note: Goa requires the use of Go modules.
 
-Current Release: `v3.7.10`
+Current Release: `v3.7.11`
 
 ## Teaser
 
@@ -315,7 +315,7 @@ In particular the page
 explains how to leverage the generated code to implement an HTTP or gRPC
 service.
 
-The [![DSL GoDoc](https://img.shields.io/badge/godoc-DSL-blue)](https://pkg.go.dev/goa.design/goa/v3@v3.7.10/dsl?tab=doc)
+The [![DSL GoDoc](https://img.shields.io/badge/godoc-DSL-blue)](https://pkg.go.dev/goa.design/goa/v3@v3.7.11/dsl?tab=doc)
 contains a fully documented reference of all the DSL functions.
 
 ### Instrumentation and System Example
diff --git a/codegen/service/service.go b/codegen/service/service.go
index c32070a8..1c525792 100644
--- a/codegen/service/service.go
+++ b/codegen/service/service.go
@@ -401,20 +401,7 @@ const unionValueMethodT = `func ({{ .TypeRef }}) {{ .Name }}() {}
 // input: map[string]{"Type": TypeData, "Error": ErrorData}
 const errorInitT = `{{ printf "%s builds a %s from an error." .Name .TypeName |  comment }}
 func {{ .Name }}(err error) {{ .TypeRef }} {
-	return &{{ .TypeName }}{
-		Name: {{ printf "%q" .ErrName }},
-		ID: goa.NewErrorID(),
-		Message: err.Error(),
-	{{- if .Temporary }}
-		Temporary: true,
-	{{- end }}
-	{{- if .Timeout }}
-		Timeout: true,
-	{{- end }}
-	{{- if .Fault }}
-		Fault: true,
-	{{- end }}
-	}
+	return goa.NewServiceError(err, {{ printf "%q" .ErrName }}, {{ printf "%v" .Timeout }}, {{ printf "%v" .Temporary}}, {{ printf "%v" .Fault}})
 }
 `
 
diff --git a/go.mod b/go.mod
index 32daadea..4f384a81 100644
--- a/go.mod
+++ b/go.mod
@@ -6,27 +6,17 @@ require (
 	github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598
 	github.com/dimfeld/httptreemux/v5 v5.4.0
 	github.com/getkin/kin-openapi v0.97.0
-	github.com/google/gxui v0.0.0-20151028112939-f85e0a97b3a4 // indirect
 	github.com/google/uuid v1.3.0
 	github.com/gorilla/websocket v1.5.0
+	github.com/hashicorp/go-multierror v1.1.1
 	github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d
-	github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b // indirect
 	github.com/pkg/errors v0.9.1
 	github.com/sergi/go-diff v1.2.0
-	github.com/smartystreets/goconvey v1.6.4 // indirect
 	github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea
-	golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
-	golang.org/x/net v0.0.0-20220708220712-1185a9018129 // indirect
-	golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d // indirect
+	golang.org/x/text v0.3.7
 	golang.org/x/tools v0.1.11
-	google.golang.org/genproto v0.0.0-20220708155623-50e5f4832e73 // indirect
 	google.golang.org/grpc v1.47.0
 	google.golang.org/protobuf v1.28.0
-	gopkg.in/yaml.v2 v2.4.0 // indirect
-)
-
-require (
-	golang.org/x/text v0.3.7
 	gopkg.in/yaml.v3 v3.0.1
 )
 
@@ -34,8 +24,17 @@ require (
 	github.com/go-openapi/jsonpointer v0.19.5 // indirect
 	github.com/go-openapi/swag v0.19.13 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
+	github.com/google/gxui v0.0.0-20151028112939-f85e0a97b3a4 // indirect
+	github.com/hashicorp/errwrap v1.0.0 // indirect
 	github.com/invopop/yaml v0.1.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/mailru/easyjson v0.7.6 // indirect
+	github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b // indirect
+	github.com/smartystreets/goconvey v1.7.2 // indirect
+	golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
+	golang.org/x/net v0.0.0-20220708220712-1185a9018129 // indirect
+	golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d // indirect
 	golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
+	google.golang.org/genproto v0.0.0-20220708155623-50e5f4832e73 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
 )
diff --git a/go.sum b/go.sum
index b8f2c5b9..04f12a40 100644
--- a/go.sum
+++ b/go.sum
@@ -68,6 +68,10 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
 github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
 github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=
 github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -97,10 +101,10 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
 github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
-github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
-github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
-github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
-github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
+github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
+github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
+github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
diff --git a/pkg/error.go b/pkg/error.go
index 25ac470d..b057f99c 100644
--- a/pkg/error.go
+++ b/pkg/error.go
@@ -6,6 +6,8 @@ import (
 	"fmt"
 	"io"
 	"strings"
+
+	"github.com/hashicorp/go-multierror"
 )
 
 type (
@@ -29,6 +31,8 @@ type (
 		// History tracks all the individual errors that were built into this error, should
 		// this error have been merged.
 		history []ServiceError
+		// err holds the original error if exists.
+		err error
 	}
 )
 
@@ -49,6 +53,19 @@ const (
 	InvalidLength = "invalid_length"
 )
 
+// NewServiceError creates an error.
+func NewServiceError(err error, name string, timeout, temporary, fault bool) *ServiceError {
+	return &ServiceError{
+		Name:      name,
+		ID:        NewErrorID(),
+		Message:   err.Error(),
+		Timeout:   timeout,
+		Temporary: temporary,
+		Fault:     fault,
+		err:       err,
+	}
+}
+
 // Fault creates an error given a format and values a la fmt.Printf. The error
 // has the Fault field set to true.
 func Fault(format string, v ...interface{}) *ServiceError {
@@ -205,6 +222,7 @@ func MergeErrors(err, other error) error {
 	//
 	// Do this before we modify ourselves, as History() may include us!
 	e.history = append(e.History(), o.History()...)
+	e.err = multierror.Append(e.err, o.err)
 
 	e.Message = e.Message + "; " + o.Message
 	e.Timeout = e.Timeout && o.Timeout
@@ -229,6 +247,8 @@ func (e *ServiceError) Error() string { return e.Message }
 // ErrorName returns the error name.
 func (e *ServiceError) ErrorName() string { return e.Name }
 
+func (e *ServiceError) Unwrap() error { return e.err }
+
 func withField(field string, err *ServiceError) *ServiceError {
 	err.Field = &field
 	return err
@@ -253,6 +273,7 @@ func asError(err error) *ServiceError {
 			ID:      NewErrorID(),
 			Message: err.Error(),
 			Fault:   true, // Default to fault for unexpected errors
+			err:     err,
 		}
 	}
 	return e
diff --git a/pkg/version.go b/pkg/version.go
index 4a4ee2d6..4c020d4d 100644
--- a/pkg/version.go
+++ b/pkg/version.go
@@ -12,7 +12,7 @@ const (
 	// Minor version number
 	Minor = 7
 	// Build number
-	Build = 10
+	Build = 11
 	// Suffix - set to empty string in release tag commits.
 	Suffix = ""
 )


Test patch:
diff --git a/codegen/service/testdata/service_code.go b/codegen/service/testdata/service_code.go
index 497df881..80d5c921 100644
--- a/codegen/service/testdata/service_code.go
+++ b/codegen/service/testdata/service_code.go
@@ -364,11 +364,7 @@ var MethodNames = [1]string{"A"}
 
 // MakeError builds a goa.ServiceError from an error.
 func MakeError(err error) *goa.ServiceError {
-	return &goa.ServiceError{
-		Name:    "error",
-		ID:      goa.NewErrorID(),
-		Message: err.Error(),
-	}
+	return goa.NewServiceError(err, "error", false, false, false)
 }
 `