Skip to content

Commit 90dc658

Browse files
authored
feat(command): use ShouldCaptureError func to capture command errors (#63)
* feat(command): turn CaptureErrors into a ShouldCaptureError function * chore: add new entry to CHANGELOG
1 parent 3e8f966 commit 90dc658

3 files changed

Lines changed: 65 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ project adheres to [Semantic Versioning](https://semver.org/).
1414
- Existing `Event-Id` value in Event Metadata does not get overwritten in correlation.EventStoreWrapper.
1515
- `postgres.EventStore` now uses the `Serde` interface for serializing to and deserializing from byte array.
1616
- `postgres.Registry` is now called `postgres.JSONRegistry` and implements thenew `postgres.Serde` interface.
17+
- `CaptureErrors` in `command.ErrorRecorder` is now a function (`ShouldCaptureError`), to allow for a more flexible capture strategy.
1718

1819
### Deprecated
1920
- ...

command/error_recorder.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ type ErrorRecorder struct {
2626
// Appender is the Event Store instance used for appending domain events.
2727
Appender eventstore.Appender
2828

29-
// CaptureErrors specifies whether an error reported from the
30-
// command.Handler should be captured (or "silenced") by this component
31-
// and return nil instead.
29+
// ShouldCaptureError is a function that should specify
30+
// whether an error reported from the command.Handler should be captured (or "silenced")
31+
// by this component and avoid returning it to the command caller.
3232
//
3333
// Default behavior is to return all errors from the command.Handler
3434
// to the caller.
35-
CaptureErrors bool
35+
ShouldCaptureError func(err error) bool
3636

3737
// StreamType specifies the stream type used for the events produced by this component.
3838
// If unspecified, FailedCommandType will be used by default.
@@ -75,10 +75,10 @@ func (er ErrorRecorder) buildStreamID(cmd eventually.Command) stream.ID {
7575
// in case of failure, appends the error and command to the Event Store
7676
// using the user-provided EventMapper.
7777
//
78-
// If CaptureError has been set to "false", the error coming from the command.Handler
78+
// If ShouldCaptureError has been set and returns "false", the error coming from the command.Handler
7979
// will be returned to the caller.
8080
//
81-
// If CaptureError has been set to "true", the error from the command.Handler is silenced
81+
// If ShouldCaptureError has been set and returns "true", the error from the command.Handler is silenced
8282
// but an error can still be returned if the append operation on the Event Store fails.
8383
func (er ErrorRecorder) Handle(ctx context.Context, cmd eventually.Command) error {
8484
err := er.Handler.Handle(ctx, cmd)
@@ -91,13 +91,18 @@ func (er ErrorRecorder) Handle(ctx context.Context, cmd eventually.Command) erro
9191
Payload: er.EventMapper(err, cmd),
9292
}
9393

94+
captureError := false
95+
if er.ShouldCaptureError != nil {
96+
captureError = er.ShouldCaptureError(err)
97+
}
98+
9499
_, appendErr := er.Appender.Append(ctx, streamID, eventstore.VersionCheckAny, event)
95-
if appendErr != nil && er.CaptureErrors {
100+
if appendErr != nil && captureError {
96101
// Append error only returned if silencing command.Handler errors.
97102
return fmt.Errorf("command.ErrorRecorder: failed to append command error to event store: %w", err)
98103
}
99104

100-
if !er.CaptureErrors {
105+
if !captureError {
101106
return fmt.Errorf("command.ErrorRecorder: command handler failed: %w", err)
102107
}
103108

command/error_recorder_test.go

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ func TestErrorRecorder(t *testing.T) {
9999
}, trackingEventStore.Recorded())
100100
})
101101

102-
t.Run("when handler fails and CaptureError is set to true, no error is returned", func(t *testing.T) {
102+
t.Run("when handler fails and ShouldCaptureError returns true, no error is returned", func(t *testing.T) {
103103
eventStore := inmemory.NewEventStore()
104104
trackingEventStore := inmemory.NewTrackingEventStore(eventStore)
105105

@@ -109,11 +109,13 @@ func TestErrorRecorder(t *testing.T) {
109109
}
110110

111111
handler := command.ErrorRecorder{
112-
CaptureErrors: true,
113112
Handler: command.HandlerFunc(func(ctx context.Context, cmd eventually.Command) error {
114113
return expectedErr
115114
}),
116115
Appender: trackingEventStore,
116+
ShouldCaptureError: func(err error) bool {
117+
return true
118+
},
117119
EventMapper: func(err error, cmd eventually.Command) eventually.Payload {
118120
return mockCommandHasFailed{
119121
err: err,
@@ -142,6 +144,53 @@ func TestErrorRecorder(t *testing.T) {
142144
}, trackingEventStore.Recorded())
143145
})
144146

147+
t.Run("when handler fails and ShouldCaptureError returns false, error is returned to caller", func(t *testing.T) {
148+
eventStore := inmemory.NewEventStore()
149+
trackingEventStore := inmemory.NewTrackingEventStore(eventStore)
150+
151+
expectedErr := errors.New("failed command")
152+
unexpectedErr := errors.New("unexpected error")
153+
154+
expectedCommand := eventually.Command{
155+
Payload: mockCommand{message: t.Name()},
156+
}
157+
158+
handler := command.ErrorRecorder{
159+
Handler: command.HandlerFunc(func(ctx context.Context, cmd eventually.Command) error {
160+
return expectedErr
161+
}),
162+
Appender: trackingEventStore,
163+
ShouldCaptureError: func(err error) bool {
164+
return errors.Is(err, unexpectedErr) // This will return false
165+
},
166+
EventMapper: func(err error, cmd eventually.Command) eventually.Payload {
167+
return mockCommandHasFailed{
168+
err: err,
169+
command: cmd.Payload.(mockCommand),
170+
}
171+
},
172+
}
173+
174+
err := handler.Handle(context.Background(), expectedCommand)
175+
176+
assert.ErrorIs(t, err, expectedErr)
177+
assert.Equal(t, []eventstore.Event{
178+
{
179+
Version: 1,
180+
Stream: stream.ID{
181+
Type: command.FailedType,
182+
Name: expectedCommand.Payload.Name(),
183+
},
184+
Event: eventually.Event{
185+
Payload: mockCommandHasFailed{
186+
err: expectedErr,
187+
command: expectedCommand.Payload.(mockCommand),
188+
},
189+
},
190+
},
191+
}, trackingEventStore.Recorded())
192+
})
193+
145194
t.Run("when handler fails, record event with custom stream type", func(t *testing.T) {
146195
eventStore := inmemory.NewEventStore()
147196
trackingEventStore := inmemory.NewTrackingEventStore(eventStore)

0 commit comments

Comments
 (0)