Skip to content

Commit c49707a

Browse files
committed
Add support for custom error messages
Add a function to each defined error to set a custom message. Signed-off-by: Derek McGowan <derek@mcg.dev>
1 parent 6c7f402 commit c49707a

File tree

4 files changed

+204
-0
lines changed

4 files changed

+204
-0
lines changed

errors.go

+85
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ func (errUnknown) Error() string { return "unknown" }
6969

7070
func (errUnknown) Unknown() {}
7171

72+
func (e errUnknown) WithMessage(msg string) error {
73+
return customMessage{e, msg}
74+
}
75+
7276
// unknown maps to Moby's "ErrUnknown"
7377
type unknown interface {
7478
Unknown()
@@ -86,6 +90,10 @@ func (errInvalidArgument) Error() string { return "invalid argument" }
8690

8791
func (errInvalidArgument) InvalidParameter() {}
8892

93+
func (e errInvalidArgument) WithMessage(msg string) error {
94+
return customMessage{e, msg}
95+
}
96+
8997
// invalidParameter maps to Moby's "ErrInvalidParameter"
9098
type invalidParameter interface {
9199
InvalidParameter()
@@ -113,6 +121,10 @@ func (errNotFound) Error() string { return "not found" }
113121

114122
func (errNotFound) NotFound() {}
115123

124+
func (e errNotFound) WithMessage(msg string) error {
125+
return customMessage{e, msg}
126+
}
127+
116128
// notFound maps to Moby's "ErrNotFound"
117129
type notFound interface {
118130
NotFound()
@@ -127,6 +139,10 @@ type errAlreadyExists struct{}
127139

128140
func (errAlreadyExists) Error() string { return "already exists" }
129141

142+
func (e errAlreadyExists) WithMessage(msg string) error {
143+
return customMessage{e, msg}
144+
}
145+
130146
// IsAlreadyExists returns true if the error is due to an already existing
131147
// metadata item
132148
func IsAlreadyExists(err error) bool {
@@ -137,6 +153,10 @@ type errPermissionDenied struct{}
137153

138154
func (errPermissionDenied) Error() string { return "permission denied" }
139155

156+
func (e errPermissionDenied) WithMessage(msg string) error {
157+
return customMessage{e, msg}
158+
}
159+
140160
// forbidden maps to Moby's "ErrForbidden"
141161
type forbidden interface {
142162
Forbidden()
@@ -152,6 +172,10 @@ type errResourceExhausted struct{}
152172

153173
func (errResourceExhausted) Error() string { return "resource exhausted" }
154174

175+
func (e errResourceExhausted) WithMessage(msg string) error {
176+
return customMessage{e, msg}
177+
}
178+
155179
// IsResourceExhausted returns true if the error is due to
156180
// a lack of resources or too many attempts.
157181
func IsResourceExhausted(err error) bool {
@@ -162,6 +186,10 @@ type errFailedPrecondition struct{}
162186

163187
func (e errFailedPrecondition) Error() string { return "failed precondition" }
164188

189+
func (e errFailedPrecondition) WithMessage(msg string) error {
190+
return customMessage{e, msg}
191+
}
192+
165193
// IsFailedPrecondition returns true if an operation could not proceed due to
166194
// the lack of a particular condition
167195
func IsFailedPrecondition(err error) bool {
@@ -174,6 +202,10 @@ func (errConflict) Error() string { return "conflict" }
174202

175203
func (errConflict) Conflict() {}
176204

205+
func (e errConflict) WithMessage(msg string) error {
206+
return customMessage{e, msg}
207+
}
208+
177209
// conflict maps to Moby's "ErrConflict"
178210
type conflict interface {
179211
Conflict()
@@ -191,6 +223,10 @@ func (errNotModified) Error() string { return "not modified" }
191223

192224
func (errNotModified) NotModified() {}
193225

226+
func (e errNotModified) WithMessage(msg string) error {
227+
return customMessage{e, msg}
228+
}
229+
194230
// notModified maps to Moby's "ErrNotModified"
195231
type notModified interface {
196232
NotModified()
@@ -206,6 +242,10 @@ type errAborted struct{}
206242

207243
func (errAborted) Error() string { return "aborted" }
208244

245+
func (e errAborted) WithMessage(msg string) error {
246+
return customMessage{e, msg}
247+
}
248+
209249
// IsAborted returns true if an operation was aborted.
210250
func IsAborted(err error) bool {
211251
return errors.Is(err, errAborted{})
@@ -215,6 +255,10 @@ type errOutOfRange struct{}
215255

216256
func (errOutOfRange) Error() string { return "out of range" }
217257

258+
func (e errOutOfRange) WithMessage(msg string) error {
259+
return customMessage{e, msg}
260+
}
261+
218262
// IsOutOfRange returns true if an operation could not proceed due
219263
// to data being out of the expected range.
220264
func IsOutOfRange(err error) bool {
@@ -227,6 +271,10 @@ func (errNotImplemented) Error() string { return "not implemented" }
227271

228272
func (errNotImplemented) NotImplemented() {}
229273

274+
func (e errNotImplemented) WithMessage(msg string) error {
275+
return customMessage{e, msg}
276+
}
277+
230278
// notImplemented maps to Moby's "ErrNotImplemented"
231279
type notImplemented interface {
232280
NotImplemented()
@@ -243,6 +291,10 @@ func (errInternal) Error() string { return "internal" }
243291

244292
func (errInternal) System() {}
245293

294+
func (e errInternal) WithMessage(msg string) error {
295+
return customMessage{e, msg}
296+
}
297+
246298
// system maps to Moby's "ErrSystem"
247299
type system interface {
248300
System()
@@ -259,6 +311,10 @@ func (errUnavailable) Error() string { return "unavailable" }
259311

260312
func (errUnavailable) Unavailable() {}
261313

314+
func (e errUnavailable) WithMessage(msg string) error {
315+
return customMessage{e, msg}
316+
}
317+
262318
// unavailable maps to Moby's "ErrUnavailable"
263319
type unavailable interface {
264320
Unavailable()
@@ -275,6 +331,10 @@ func (errDataLoss) Error() string { return "data loss" }
275331

276332
func (errDataLoss) DataLoss() {}
277333

334+
func (e errDataLoss) WithMessage(msg string) error {
335+
return customMessage{e, msg}
336+
}
337+
278338
// dataLoss maps to Moby's "ErrDataLoss"
279339
type dataLoss interface {
280340
DataLoss()
@@ -291,6 +351,10 @@ func (errUnauthorized) Error() string { return "unauthorized" }
291351

292352
func (errUnauthorized) Unauthorized() {}
293353

354+
func (e errUnauthorized) WithMessage(msg string) error {
355+
return customMessage{e, msg}
356+
}
357+
294358
// unauthorized maps to Moby's "ErrUnauthorized"
295359
type unauthorized interface {
296360
Unauthorized()
@@ -307,6 +371,8 @@ func isInterface[T any](err error) bool {
307371
switch x := err.(type) {
308372
case T:
309373
return true
374+
case customMessage:
375+
err = x.err
310376
case interface{ Unwrap() error }:
311377
err = x.Unwrap()
312378
if err == nil {
@@ -324,3 +390,22 @@ func isInterface[T any](err error) bool {
324390
}
325391
}
326392
}
393+
394+
// customMessage is used to provide a defined error with a custom message.
395+
// The message is not wrapped but can be compared by the `Is(error) bool` interface.
396+
type customMessage struct {
397+
err error
398+
msg string
399+
}
400+
401+
func (c customMessage) Is(err error) bool {
402+
return c.err == err
403+
}
404+
405+
func (c customMessage) As(target any) bool {
406+
return errors.As(c.err, target)
407+
}
408+
409+
func (c customMessage) Error() string {
410+
return c.msg
411+
}

errors_test.go

+114
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package errdefs
1919
import (
2020
"context"
2121
"errors"
22+
"reflect"
2223
"testing"
2324
)
2425

@@ -44,6 +45,119 @@ func TestInvalidArgument(t *testing.T) {
4445
}
4546
}
4647

48+
func TestErrorEquivalence(t *testing.T) {
49+
var e1 error = ErrAborted
50+
var e2 error = ErrUnknown
51+
if e1 == e2 {
52+
t.Fatal("should not equal the same error")
53+
}
54+
if errors.Is(e1, e2) {
55+
t.Fatal("errors.Is should not return true")
56+
}
57+
58+
var e3 error = errAborted{}
59+
if e1 != e3 {
60+
t.Fatal("new instance should be equivalent")
61+
}
62+
if !errors.Is(e1, e3) {
63+
t.Fatal("errors.Is should be true")
64+
}
65+
if !errors.Is(e3, e1) {
66+
t.Fatal("errors.Is should be true")
67+
}
68+
var aborted errAborted
69+
if !errors.As(e1, &aborted) {
70+
t.Fatal("errors.As should be true")
71+
}
72+
73+
var e4 = ErrAborted.WithMessage("custom message")
74+
if e1 == e4 {
75+
t.Fatal("should not equal the same error")
76+
}
77+
78+
if !errors.Is(e4, e1) {
79+
t.Fatal("errors.Is should not return true")
80+
}
81+
82+
if errors.Is(e1, e4) {
83+
t.Fatal("errors.Is should be false, e1 is not a custom message")
84+
}
85+
86+
if !errors.As(e4, &aborted) {
87+
t.Fatal("errors.As should be true")
88+
}
89+
90+
var custom customMessage
91+
if !errors.As(e4, &custom) {
92+
t.Fatal("errors.As should be true")
93+
}
94+
if custom.msg != "custom message" {
95+
t.Fatalf("unexpected custom message: %q", custom.msg)
96+
}
97+
if custom.err != e1 {
98+
t.Fatalf("unexpected custom message error: %v", custom.err)
99+
}
100+
}
101+
102+
func TestWithMessage(t *testing.T) {
103+
testErrors := []error{ErrUnknown,
104+
ErrInvalidArgument,
105+
ErrNotFound,
106+
ErrAlreadyExists,
107+
ErrPermissionDenied,
108+
ErrResourceExhausted,
109+
ErrFailedPrecondition,
110+
ErrConflict,
111+
ErrNotModified,
112+
ErrAborted,
113+
ErrOutOfRange,
114+
ErrNotImplemented,
115+
ErrInternal,
116+
ErrUnavailable,
117+
ErrDataLoss,
118+
ErrUnauthenticated,
119+
}
120+
for _, err := range testErrors {
121+
e1 := err
122+
t.Run(err.Error(), func(t *testing.T) {
123+
wm, ok := e1.(interface{ WithMessage(string) error })
124+
if !ok {
125+
t.Fatal("WithMessage not supported")
126+
}
127+
e2 := wm.WithMessage("custom message")
128+
129+
if e1 == e2 {
130+
t.Fatal("should not equal the same error")
131+
}
132+
133+
if !errors.Is(e2, e1) {
134+
t.Fatal("errors.Is should not return true")
135+
}
136+
137+
if errors.Is(e1, e2) {
138+
t.Fatal("errors.Is should be false, e1 is not a custom message")
139+
}
140+
141+
var raw = reflect.New(reflect.TypeOf(e1)).Interface()
142+
if !errors.As(e2, raw) {
143+
t.Fatal("errors.As should be true")
144+
}
145+
146+
var custom customMessage
147+
if !errors.As(e2, &custom) {
148+
t.Fatal("errors.As should be true")
149+
}
150+
if custom.msg != "custom message" {
151+
t.Fatalf("unexpected custom message: %q", custom.msg)
152+
}
153+
if custom.err != e1 {
154+
t.Fatalf("unexpected custom message error: %v", custom.err)
155+
}
156+
157+
})
158+
}
159+
}
160+
47161
type customInvalidArgument struct{}
48162

49163
func (*customInvalidArgument) Error() string {

resolve.go

+2
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ func firstError(err error) error {
6666
return err
6767
}
6868
switch e := err.(type) {
69+
case customMessage:
70+
err = e.err
6971
case unknown:
7072
return ErrUnknown
7173
case invalidParameter:

resolve_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ func TestResolve(t *testing.T) {
6161
{errors.Join(testUnavailable{}, ErrPermissionDenied), ErrUnavailable},
6262
{errors.Join(errors.New("untyped join")), ErrUnknown},
6363
{errors.Join(errors.New("untyped1"), errors.New("untyped2")), ErrUnknown},
64+
{ErrNotFound.WithMessage("something else"), ErrNotFound},
65+
{wrap(ErrNotFound.WithMessage("something else")), ErrNotFound},
66+
{errors.Join(ErrNotFound.WithMessage("something else"), ErrPermissionDenied), ErrNotFound},
6467
} {
6568
name := fmt.Sprintf("%d-%s", i, errorString(tc.resolved))
6669
tc := tc

0 commit comments

Comments
 (0)