Repository: open-feature/go-sdk
Pull request #82 (2022-09-28 16:04:31)

feat!: spec v0.5.0 compliance
Signed-off-by: Skye Gill <gill.skye95@gmail.com>

codecov-commenter: # [Codecov](https://codecov.io/gh/open-feature/go-sdk/pull/82?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) Report
> Merging [#82](https://codecov.io/gh/open-feature/go-sdk/pull/82?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) (a97c889) into [main](https://codecov.io/gh/open-feature/go-sdk/commit/fc4b8716f6d3c904b464d34176d0c6ed67f741fc?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) (fc4b871) will **decrease** coverage by `4.70%`.
> The diff coverage is `53.12%`.

```diff
@@            Coverage Diff             @@
##             main      #82      +/-   ##
==========================================
- Coverage   80.14%   75.43%   -4.71%     
==========================================
  Files           6        7       +1     
  Lines         418      460      +42     
==========================================
+ Hits          335      347      +12     
- Misses         70      100      +30     
  Partials       13       13              
```


| [Impacted Files](https://codecov.io/gh/open-feature/go-sdk/pull/82?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) | Coverage Δ | |
|---|---|---|
| [pkg/openfeature/resolution\_error.go](https://codecov.io/gh/open-feature/go-sdk/pull/82/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-cGtnL29wZW5mZWF0dXJlL3Jlc29sdXRpb25fZXJyb3IuZ28=) | `14.28% <14.28%> (ø)` | |
| [pkg/openfeature/client.go](https://codecov.io/gh/open-feature/go-sdk/pull/82/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-cGtnL29wZW5mZWF0dXJlL2NsaWVudC5nbw==) | `78.52% <100.00%> (ø)` | |
| [pkg/openfeature/noop\_provider.go](https://codecov.io/gh/open-feature/go-sdk/pull/82/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-cGtnL29wZW5mZWF0dXJlL25vb3BfcHJvdmlkZXIuZ28=) | `100.00% <100.00%> (ø)` | |
| [pkg/openfeature/provider.go](https://codecov.io/gh/open-feature/go-sdk/pull/82/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-cGtnL29wZW5mZWF0dXJlL3Byb3ZpZGVyLmdv) | `100.00% <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=open-feature)


Patch:
diff --git a/README.md b/README.md
index 3938854..8328659 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
 [![a](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1)
 [![Go Report Card](https://goreportcard.com/badge/github.com/open-feature/go-sdk)](https://goreportcard.com/report/github.com/open-feature/go-sdk)
 [![codecov](https://codecov.io/gh/open-feature/go-sdk/branch/main/graph/badge.svg?token=FZ17BHNSU5)](https://codecov.io/gh/open-feature/go-sdk)
-[![Specification](https://img.shields.io/static/v1?label=Specification&message=v0.4.0&color=yellow)](https://github.com/open-feature/spec/tree/v0.4.0)
+[![Specification](https://img.shields.io/static/v1?label=Specification&message=v0.5.0&color=yellow)](https://github.com/open-feature/spec/tree/v0.5.0)
 
 This is the Go implementation of [OpenFeature](https://openfeature.dev), a vendor-agnostic abstraction library for evaluating feature flags.
 
diff --git a/pkg/openfeature/client.go b/pkg/openfeature/client.go
index f762473..65328bb 100644
--- a/pkg/openfeature/client.go
+++ b/pkg/openfeature/client.go
@@ -110,7 +110,15 @@ var typeToString = map[Type]string{
 type EvaluationDetails struct {
 	FlagKey  string
 	FlagType Type
-	InterfaceResolutionDetail
+	Value    interface{}
+	ResolutionDetail
+}
+
+type ResolutionDetail struct {
+	Variant      string
+	Reason       Reason
+	ErrorCode    ErrorCode
+	ErrorMessage string
 }
 
 // Option applies a change to EvaluationOptions
@@ -382,9 +390,7 @@ func (c Client) evaluate(
 	evalDetails := EvaluationDetails{
 		FlagKey:  flag,
 		FlagType: flagType,
-		InterfaceResolutionDetail: InterfaceResolutionDetail{
-			Value: defaultValue,
-		},
+		Value:    defaultValue,
 	}
 
 	apiClientInvocationProviderHooks := append(append(append(api.hooks, c.hooks...), options.hooks...), api.provider.Hooks()...) // API, Client, Invocation, Provider
@@ -413,22 +419,22 @@ func (c Client) evaluate(
 	case Boolean:
 		defValue := defaultValue.(bool)
 		res := api.provider.BooleanEvaluation(ctx, flag, defValue, flatCtx)
-		resolution.ResolutionDetail = res.ResolutionDetail
+		resolution.ProviderResolutionDetail = res.ProviderResolutionDetail
 		resolution.Value = res.Value
 	case String:
 		defValue := defaultValue.(string)
 		res := api.provider.StringEvaluation(ctx, flag, defValue, flatCtx)
-		resolution.ResolutionDetail = res.ResolutionDetail
+		resolution.ProviderResolutionDetail = res.ProviderResolutionDetail
 		resolution.Value = res.Value
 	case Float:
 		defValue := defaultValue.(float64)
 		res := api.provider.FloatEvaluation(ctx, flag, defValue, flatCtx)
-		resolution.ResolutionDetail = res.ResolutionDetail
+		resolution.ProviderResolutionDetail = res.ProviderResolutionDetail
 		resolution.Value = res.Value
 	case Int:
 		defValue := defaultValue.(int64)
 		res := api.provider.IntEvaluation(ctx, flag, defValue, flatCtx)
-		resolution.ResolutionDetail = res.ResolutionDetail
+		resolution.ProviderResolutionDetail = res.ProviderResolutionDetail
 		resolution.Value = res.Value
 	}
 
@@ -437,14 +443,16 @@ func (c Client) evaluate(
 		c.logger().Error(
 			err, "flag resolution", "flag", flag, "defaultValue", defaultValue,
 			"evaluationContext", evalCtx, "evaluationOptions", options, "type", flagType.String(), "errorCode", err,
+			"errMessage", resolution.ResolutionError.message,
 		)
 		err = fmt.Errorf("error code: %w", err)
 		c.errorHooks(hookCtx, providerInvocationClientApiHooks, err, options)
-		evalDetails.InterfaceResolutionDetail.ResolutionDetail = resolution.ResolutionDetail
+		evalDetails.ResolutionDetail = resolution.ResolutionDetail()
+		evalDetails.Reason = ErrorReason
 		return evalDetails, err
 	}
 	if resolution.Value != nil {
-		evalDetails.InterfaceResolutionDetail = resolution
+		evalDetails.ResolutionDetail = resolution.ResolutionDetail()
 	}
 
 	if err := c.afterHooks(hookCtx, providerInvocationClientApiHooks, evalDetails, options); err != nil {
diff --git a/pkg/openfeature/noop_provider.go b/pkg/openfeature/noop_provider.go
index 5cd5561..85df437 100644
--- a/pkg/openfeature/noop_provider.go
+++ b/pkg/openfeature/noop_provider.go
@@ -14,9 +14,9 @@ func (e NoopProvider) Metadata() Metadata {
 func (e NoopProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx FlattenedContext) BoolResolutionDetail {
 	return BoolResolutionDetail{
 		Value: defaultValue,
-		ResolutionDetail: ResolutionDetail{
+		ProviderResolutionDetail: ProviderResolutionDetail{
 			Variant: "default-variant",
-			Reason:  DEFAULT,
+			Reason:  DefaultReason,
 		},
 	}
 }
@@ -25,9 +25,9 @@ func (e NoopProvider) BooleanEvaluation(ctx context.Context, flag string, defaul
 func (e NoopProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx FlattenedContext) StringResolutionDetail {
 	return StringResolutionDetail{
 		Value: defaultValue,
-		ResolutionDetail: ResolutionDetail{
+		ProviderResolutionDetail: ProviderResolutionDetail{
 			Variant: "default-variant",
-			Reason:  DEFAULT,
+			Reason:  DefaultReason,
 		},
 	}
 }
@@ -36,9 +36,9 @@ func (e NoopProvider) StringEvaluation(ctx context.Context, flag string, default
 func (e NoopProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx FlattenedContext) FloatResolutionDetail {
 	return FloatResolutionDetail{
 		Value: defaultValue,
-		ResolutionDetail: ResolutionDetail{
+		ProviderResolutionDetail: ProviderResolutionDetail{
 			Variant: "default-variant",
-			Reason:  DEFAULT,
+			Reason:  DefaultReason,
 		},
 	}
 }
@@ -47,9 +47,9 @@ func (e NoopProvider) FloatEvaluation(ctx context.Context, flag string, defaultV
 func (e NoopProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx FlattenedContext) IntResolutionDetail {
 	return IntResolutionDetail{
 		Value: defaultValue,
-		ResolutionDetail: ResolutionDetail{
+		ProviderResolutionDetail: ProviderResolutionDetail{
 			Variant: "default-variant",
-			Reason:  DEFAULT,
+			Reason:  DefaultReason,
 		},
 	}
 }
@@ -58,9 +58,9 @@ func (e NoopProvider) IntEvaluation(ctx context.Context, flag string, defaultVal
 func (e NoopProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx FlattenedContext) InterfaceResolutionDetail {
 	return InterfaceResolutionDetail{
 		Value: defaultValue,
-		ResolutionDetail: ResolutionDetail{
+		ProviderResolutionDetail: ProviderResolutionDetail{
 			Variant: "default-variant",
-			Reason:  DEFAULT,
+			Reason:  DefaultReason,
 		},
 	}
 }
diff --git a/pkg/openfeature/provider.go b/pkg/openfeature/provider.go
index de85a71..560cd6a 100644
--- a/pkg/openfeature/provider.go
+++ b/pkg/openfeature/provider.go
@@ -6,12 +6,18 @@ import (
 )
 
 const (
-	DISABLED        string = "disabled"     // variant returned because feature is disabled
-	TARGETING_MATCH string = "target match" // variant returned because matched target rule
-	SPLIT           string = "split"        // variant returned because of pseudorandom assignment
-	DEFAULT         string = "default"      // variant returned the default
-	UNKNOWN         string = "unknown"      // variant returned for unknown reason
-	ERROR           string = "error"        // variant returned due to error
+	// DefaultReason - the resolved value was configured statically, or otherwise fell back to a pre-configured value.
+	DefaultReason Reason = "DEFAULT"
+	// TargetingMatchReason - the resolved value was the result of a dynamic evaluation, such as a rule or specific user-targeting.
+	TargetingMatchReason Reason = "TARGETING_MATCH"
+	// SplitReason - the resolved value was the result of pseudorandom assignment.
+	SplitReason Reason = "SPLIT"
+	// DisabledReason - the resolved value was the result of the flag being disabled in the management system.
+	DisabledReason Reason = "DISABLED"
+	// UnknownReason - the reason for the resolved value could not be determined.
+	UnknownReason Reason = "UNKNOWN"
+	// ErrorReason - the resolved value was the result of an error.
+	ErrorReason Reason = "ERROR"
 
 	TargetingKey string = "targetingKey" // evaluation context map key. The targeting key uniquely identifies the subject (end-user, or client service) of a flag evaluation.
 )
@@ -20,6 +26,9 @@ const (
 // TargetingKey ("targetingKey") is stored as a string value if provided in the evaluation context.
 type FlattenedContext map[string]interface{}
 
+// Reason indicates the semantic reason for a returned flag value
+type Reason string
+
 // FeatureProvider interface defines a set of functions that can be called in order to evaluate a flag.
 // vendors should implement
 type FeatureProvider interface {
@@ -32,52 +41,61 @@ type FeatureProvider interface {
 	Hooks() []Hook
 }
 
-// ResolutionDetail is a structure which contains a subset of the fields defined in the EvaluationDetail,
+// ProviderResolutionDetail is a structure which contains a subset of the fields defined in the EvaluationDetail,
 // representing the result of the provider's flag resolution process
 // see https://github.com/open-feature/spec/blob/main/specification/types.md#resolution-details
 // N.B we could use generics but to support older versions of go for now we will have type specific resolution
 // detail
-type ResolutionDetail struct {
-	ErrorCode string
-	Reason    string
-	Variant   string
+type ProviderResolutionDetail struct {
+	ResolutionError ResolutionError
+	Reason          Reason
+	Variant         string
+}
+
+func (p ProviderResolutionDetail) ResolutionDetail() ResolutionDetail {
+	return ResolutionDetail{
+		Variant:      p.Variant,
+		Reason:       p.Reason,
+		ErrorCode:    p.ResolutionError.code,
+		ErrorMessage: p.ResolutionError.message,
+	}
 }
 
-func (resolution ResolutionDetail) Error() error {
-	if resolution.ErrorCode == "" {
+func (p ProviderResolutionDetail) Error() error {
+	if p.ResolutionError.code == "" {
 		return nil
 	}
-	return errors.New(resolution.ErrorCode)
+	return errors.New(string(p.ResolutionError.code))
 }
 
 // BoolResolutionDetail provides a resolution detail with boolean type
 type BoolResolutionDetail struct {
 	Value bool
-	ResolutionDetail
+	ProviderResolutionDetail
 }
 
 // StringResolutionDetail provides a resolution detail with string type
 type StringResolutionDetail struct {
 	Value string
-	ResolutionDetail
+	ProviderResolutionDetail
 }
 
 // FloatResolutionDetail provides a resolution detail with float64 type
 type FloatResolutionDetail struct {
 	Value float64
-	ResolutionDetail
+	ProviderResolutionDetail
 }
 
 // IntResolutionDetail provides a resolution detail with int64 type
 type IntResolutionDetail struct {
 	Value int64
-	ResolutionDetail
+	ProviderResolutionDetail
 }
 
 // InterfaceResolutionDetail provides a resolution detail with interface{} type
 type InterfaceResolutionDetail struct {
 	Value interface{}
-	ResolutionDetail
+	ProviderResolutionDetail
 }
 
 // Metadata provides provider name
diff --git a/pkg/openfeature/resolution_error.go b/pkg/openfeature/resolution_error.go
new file mode 100644
index 0000000..612ae52
--- /dev/null
+++ b/pkg/openfeature/resolution_error.go
@@ -0,0 +1,98 @@
+package openfeature
+
+type ErrorCode string
+
+const (
+	// ProviderNotReadyCode - the value was resolved before the provider was ready.
+	ProviderNotReadyCode ErrorCode = "PROVIDER_NOT_READY"
+	// FlagNotFoundCode - the flag could not be found.
+	FlagNotFoundCode ErrorCode = "FLAG_NOT_FOUND"
+	// ParseErrorCode - an error was encountered parsing data, such as a flag configuration.
+	ParseErrorCode ErrorCode = "PARSE_ERROR"
+	// TypeMismatchCode - the type of the flag value does not match the expected type.
+	TypeMismatchCode ErrorCode = "TYPE_MISMATCH"
+	// TargetingKeyMissingCode - the provider requires a targeting key and one was not provided in the evaluation context.
+	TargetingKeyMissingCode ErrorCode = "TARGETING_KEY_MISSING"
+	// InvalidContextCode - the evaluation context does not meet provider requirements.
+	InvalidContextCode ErrorCode = "INVALID_CONTEXT"
+	// GeneralCode - the error was for a reason not enumerated above.
+	GeneralCode ErrorCode = "GENERAL"
+)
+
+// ResolutionError is an enumerated error code with an optional message
+type ResolutionError struct {
+	// fields are unexported, this means providers are forced to create structs of this type using one of the constructors below.
+	// this effectively emulates an enum
+	code    ErrorCode
+	message string
+}
+
+// NewProviderNotReadyResolutionError constructs a resolution error with code PROVIDER_NOT_READY
+//
+// Explanation - The value was resolved before the provider was ready.
+func NewProviderNotReadyResolutionError(msg string) ResolutionError {
+	return ResolutionError{
+		code:    ProviderNotReadyCode,
+		message: msg,
+	}
+}
+
+// NewFlagNotFoundResolutionError constructs a resolution error with code FLAG_NOT_FOUND
+//
+// Explanation - The flag could not be found.
+func NewFlagNotFoundResolutionError(msg string) ResolutionError {
+	return ResolutionError{
+		code:    FlagNotFoundCode,
+		message: msg,
+	}
+}
+
+// NewParseErrorResolutionError constructs a resolution error with code PARSE_ERROR
+//
+// Explanation - An error was encountered parsing data, such as a flag configuration.
+func NewParseErrorResolutionError(msg string) ResolutionError {
+	return ResolutionError{
+		code:    ParseErrorCode,
+		message: msg,
+	}
+}
+
+// NewTypeMismatchResolutionError constructs a resolution error with code TYPE_MISMATCH
+//
+// Explanation - The type of the flag value does not match the expected type.
+func NewTypeMismatchResolutionError(msg string) ResolutionError {
+	return ResolutionError{
+		code:    TypeMismatchCode,
+		message: msg,
+	}
+}
+
+// NewTargetingKeyMissingResolutionError constructs a resolution error with code TARGETING_KEY_MISSING
+//
+// Explanation - The provider requires a targeting key and one was not provided in the evaluation context.
+func NewTargetingKeyMissingResolutionError(msg string) ResolutionError {
+	return ResolutionError{
+		code:    TargetingKeyMissingCode,
+		message: msg,
+	}
+}
+
+// NewInvalidContextResolutionError constructs a resolution error with code INVALID_CONTEXT
+//
+// Explanation - The evaluation context does not meet provider requirements.
+func NewInvalidContextResolutionError(msg string) ResolutionError {
+	return ResolutionError{
+		code:    InvalidContextCode,
+		message: msg,
+	}
+}
+
+// NewGeneralResolutionError constructs a resolution error with code GENERAL
+//
+// Explanation - The error was for a reason not enumerated above.
+func NewGeneralResolutionError(msg string) ResolutionError {
+	return ResolutionError{
+		code:    GeneralCode,
+		message: msg,
+	}
+}


Test patch:
diff --git a/pkg/openfeature/client_test.go b/pkg/openfeature/client_test.go
index c5d521e..81f385d 100644
--- a/pkg/openfeature/client_test.go
+++ b/pkg/openfeature/client_test.go
@@ -190,17 +190,69 @@ func TestRequirement_1_4_4(t *testing.T) {
 //
 // Has no suitable test as the provider implementation populates the EvaluationDetails reason field
 
-// Requirement_1_4_7
-// In cases of abnormal execution, the `evaluation details` structure's `error code` field MUST
-// contain a string identifying an error occurred during flag evaluation and the nature of the error.
-//
-// Has no suitable test as the provider implementation populates the EvaluationDetails error code field
+// In cases of abnormal execution, the `evaluation details` structure's
+// `error code` field MUST contain an `error code`.
+func TestRequirement_1_4_7(t *testing.T) {
+	defer t.Cleanup(initSingleton)
+	client := NewClient("test-client")
+
+	ctrl := gomock.NewController(t)
+	mockProvider := NewMockFeatureProvider(ctrl)
+	mockProvider.EXPECT().Metadata().AnyTimes()
+	mockProvider.EXPECT().Hooks().AnyTimes()
+	mockProvider.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
+		Return(BoolResolutionDetail{
+			Value: false,
+			ProviderResolutionDetail: ProviderResolutionDetail{
+				ResolutionError: NewGeneralResolutionError("test"),
+			},
+		})
+	SetProvider(mockProvider)
+
+	res, err := client.evaluate(
+		context.Background(), "foo", Boolean, true, EvaluationContext{}, EvaluationOptions{},
+	)
+	if err == nil {
+		t.Error("expected err, got nil")
+	}
+
+	expectedErrorCode := GeneralCode
+	if res.ErrorCode != expectedErrorCode {
+		t.Errorf("expected error code to be '%s', got '%s'", expectedErrorCode, res.ErrorCode)
+	}
+}
 
-// Requirement_1_4_8
 // In cases of abnormal execution (network failure, unhandled error, etc) the `reason` field
 // in the `evaluation details` SHOULD indicate an error.
-//
-// Has no suitable test as the provider implementation populates the EvaluationDetails reason field
+func TestRequirement_1_4_8(t *testing.T) {
+	defer t.Cleanup(initSingleton)
+	client := NewClient("test-client")
+
+	ctrl := gomock.NewController(t)
+	mockProvider := NewMockFeatureProvider(ctrl)
+	mockProvider.EXPECT().Metadata().AnyTimes()
+	mockProvider.EXPECT().Hooks().AnyTimes()
+	mockProvider.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
+		Return(BoolResolutionDetail{
+			Value: false,
+			ProviderResolutionDetail: ProviderResolutionDetail{
+				ResolutionError: NewGeneralResolutionError("test"),
+			},
+		})
+	SetProvider(mockProvider)
+
+	res, err := client.evaluate(
+		context.Background(), "foo", Boolean, true, EvaluationContext{}, EvaluationOptions{},
+	)
+	if err == nil {
+		t.Error("expected err, got nil")
+	}
+
+	expectedReason := ErrorReason
+	if res.Reason != expectedReason {
+		t.Errorf("expected reason to be '%s', got '%s'", expectedReason, res.Reason)
+	}
+}
 
 // Methods, functions, or operations on the client MUST NOT throw exceptions, or otherwise abnormally terminate.
 // Flag evaluation calls must always return the `default value` in the event of abnormal execution.
@@ -226,9 +278,8 @@ func TestRequirement_1_4_9(t *testing.T) {
 		mockProvider.EXPECT().BooleanEvaluation(context.Background(), flagKey, defaultValue, flatCtx).
 			Return(BoolResolutionDetail{
 				Value: false,
-				ResolutionDetail: ResolutionDetail{
-					ErrorCode: "GENERAL",
-					Reason:    "forced test error",
+				ProviderResolutionDetail: ProviderResolutionDetail{
+					ResolutionError: NewGeneralResolutionError("test"),
 				},
 			}).Times(2)
 		SetProvider(mockProvider)
@@ -261,9 +312,8 @@ func TestRequirement_1_4_9(t *testing.T) {
 		mockProvider.EXPECT().StringEvaluation(context.Background(), flagKey, defaultValue, flatCtx).
 			Return(StringResolutionDetail{
 				Value: "foo",
-				ResolutionDetail: ResolutionDetail{
-					ErrorCode: "GENERAL",
-					Reason:    "forced test error",
+				ProviderResolutionDetail: ProviderResolutionDetail{
+					ResolutionError: NewGeneralResolutionError("test"),
 				},
 			}).Times(2)
 		SetProvider(mockProvider)
@@ -296,9 +346,8 @@ func TestRequirement_1_4_9(t *testing.T) {
 		mockProvider.EXPECT().FloatEvaluation(context.Background(), flagKey, defaultValue, flatCtx).
 			Return(FloatResolutionDetail{
 				Value: 0,
-				ResolutionDetail: ResolutionDetail{
-					ErrorCode: "GENERAL",
-					Reason:    "forced test error",
+				ProviderResolutionDetail: ProviderResolutionDetail{
+					ResolutionError: NewGeneralResolutionError("test"),
 				},
 			}).Times(2)
 		SetProvider(mockProvider)
@@ -331,9 +380,8 @@ func TestRequirement_1_4_9(t *testing.T) {
 		mockProvider.EXPECT().IntEvaluation(context.Background(), flagKey, defaultValue, flatCtx).
 			Return(IntResolutionDetail{
 				Value: 0,
-				ResolutionDetail: ResolutionDetail{
-					ErrorCode: "GENERAL",
-					Reason:    "forced test error",
+				ProviderResolutionDetail: ProviderResolutionDetail{
+					ResolutionError: NewGeneralResolutionError("test"),
 				},
 			}).Times(2)
 		SetProvider(mockProvider)
@@ -368,9 +416,8 @@ func TestRequirement_1_4_9(t *testing.T) {
 		mockProvider.EXPECT().Hooks().AnyTimes()
 		mockProvider.EXPECT().ObjectEvaluation(context.Background(), flagKey, defaultValue, flatCtx).
 			Return(InterfaceResolutionDetail{
-				ResolutionDetail: ResolutionDetail{
-					ErrorCode: "GENERAL",
-					Reason:    "forced test error",
+				ProviderResolutionDetail: ProviderResolutionDetail{
+					ResolutionError: NewGeneralResolutionError("test"),
 				},
 			}).Times(2)
 		SetProvider(mockProvider)
@@ -403,6 +450,42 @@ func TestRequirement_1_4_9(t *testing.T) {
 //
 // Satisfied by goroutines.
 
+// In cases of abnormal execution, the `evaluation details` structure's `error message` field MAY contain a
+// string containing additional details about the nature of the error.
+func TestRequirement_1_4_12(t *testing.T) {
+	defer t.Cleanup(initSingleton)
+	ctrl := gomock.NewController(t)
+
+	errMessage := "error forced by test"
+
+	mockProvider := NewMockFeatureProvider(ctrl)
+	mockProvider.EXPECT().Metadata().AnyTimes()
+	mockProvider.EXPECT().Hooks().AnyTimes()
+	SetProvider(mockProvider)
+	mockProvider.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
+		Return(BoolResolutionDetail{
+			Value: true,
+			ProviderResolutionDetail: ProviderResolutionDetail{
+				ResolutionError: NewGeneralResolutionError(errMessage),
+			},
+		})
+
+	client := NewClient("test")
+	evalDetails, err := client.evaluate(
+		context.Background(), "foo", Boolean, true, EvaluationContext{}, EvaluationOptions{},
+	)
+	if err == nil {
+		t.Error("expected err, got nil")
+	}
+
+	if evalDetails.ErrorMessage != errMessage {
+		t.Errorf(
+			"expected evaluation details to contain error message '%s', got '%s'",
+			errMessage, evalDetails.ErrorMessage,
+		)
+	}
+}
+
 // Requirement_1_5_1
 // The `evaluation options` structure's `hooks` field denotes an ordered collection of hooks that the client MUST
 // execute for the respective flag evaluation, in addition to those already configured.
@@ -549,7 +632,7 @@ func TestErrorCodeFromProviderReturnedInEvaluationDetails(t *testing.T) {
 	defer t.Cleanup(initSingleton)
 	ctrl := gomock.NewController(t)
 
-	const errorCode = "TIMEOUT"
+	generalErrorCode := GeneralCode
 
 	mockProvider := NewMockFeatureProvider(ctrl)
 	mockProvider.EXPECT().Metadata().AnyTimes()
@@ -558,10 +641,8 @@ func TestErrorCodeFromProviderReturnedInEvaluationDetails(t *testing.T) {
 	mockProvider.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
 		Return(BoolResolutionDetail{
 			Value: true,
-			ResolutionDetail: ResolutionDetail{
-				ErrorCode: errorCode,
-				Reason:    "",
-				Variant:   "",
+			ProviderResolutionDetail: ProviderResolutionDetail{
+				ResolutionError: NewGeneralResolutionError("test"),
 			},
 		})
 
@@ -573,10 +654,10 @@ func TestErrorCodeFromProviderReturnedInEvaluationDetails(t *testing.T) {
 		t.Error("expected err, got nil")
 	}
 
-	if evalDetails.ErrorCode != errorCode {
+	if evalDetails.ErrorCode != generalErrorCode {
 		t.Errorf(
 			"expected evaluation details to contain error code '%s', got '%s'",
-			errorCode, evalDetails.ErrorCode,
+			generalErrorCode, evalDetails.ErrorCode,
 		)
 	}
 }
diff --git a/pkg/openfeature/hooks_test.go b/pkg/openfeature/hooks_test.go
index b268bea..c65c62e 100644
--- a/pkg/openfeature/hooks_test.go
+++ b/pkg/openfeature/hooks_test.go
@@ -401,7 +401,11 @@ func TestRequirement_4_3_6(t *testing.T) {
 		mockHook.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).
 			After(
 				mockProvider.EXPECT().StringEvaluation(context.Background(), flagKey, defaultValue, flatCtx).
-					Return(StringResolutionDetail{ResolutionDetail: ResolutionDetail{ErrorCode: "forced"}}),
+					Return(StringResolutionDetail{
+						ProviderResolutionDetail: ProviderResolutionDetail{
+							ResolutionError: NewGeneralResolutionError("test"),
+						},
+					}),
 			)
 		mockHook.EXPECT().Finally(gomock.Any(), gomock.Any())