# I am the Watcher. I am your guide through this vast new twtiverse.
# 
# Usage:
#     https://watcher.sour.is/api/plain/users              View list of users and latest twt date.
#     https://watcher.sour.is/api/plain/twt                View all twts.
#     https://watcher.sour.is/api/plain/mentions?uri=:uri  View all mentions for uri.
#     https://watcher.sour.is/api/plain/conv/:hash         View all twts for a conversation subject.
# 
# Options:
#     uri     Filter to show a specific users twts.
#     offset  Start index for quey.
#     limit   Count of items to return (going back in time).
# 
# twt range = 1 17
# self = https://watcher.sour.is/conv/o2664qa
Question to all you Gophers out there: How do you deal with custom errors that include more information and different kinds of matching them?

I started with a simple var ErrPermissionNotAllowed = errors.New("permission not allowed"). In my function I then wrap that using fmt.Errorf("%w: %v", ErrPermissionNotAllowed, failedPermissions). I can match this error using errors.Is(err, ErrPermissionNotAllowed). So far so good.

Now for display purposes I'd also like to access the individual permissions that could not be assigned. Parsing the error message is obviously not an option. So I thought, I create a custom error type, e.g. type PermissionNotAllowedError []Permission and give it some func (e PermissionNotAllowedError) Error() string { return fmt.Sprintf("permission not allowed: %v", e) }. My function would then return this error instead: PermissionNotAllowedError{failedPermissions}

At some layers I don't care about the exact permissions that failed, but at others I do, at least when accessing them. A custom func (e PermissionNotAllowedError) Is(target err) bool could match both the general ErrPermissionNotAllowed as well as the PermissionNotAllowedError. Same with As(…). For testing purposes the PermissionNotAllowedError would then also try to match the included permissions, so assertions in tests would work nicely. But having two different errors for different matching seems not very elegant at all.

Did you ever encounter this scenario before? How did you address this? Is my thinking flawed?
@lyse This is exactly how I would approach it 👌
@lyse This is exactly how I would approach it 👌
@lyse This is exactly how I would approach it 👌
@prologic Thanks for confirming, but it just sounds like an ugly hack. Even after sleeping on this. I feel there must be something elegant. Error handling in Go is still not mature.
Why not just always use the second one?
Why not just always use the second one?
You can have Error return just "permission not allowed" if the array is empty. It would print the same as the first.
You can have Error return just "permission not allowed" if the array is empty. It would print the same as the first.
@xuu Wouldn't my Is check for array equality, too? At least that would be great for unit tests. Like this untested piece of code:

func (e PermissionsNotAllowedError) Is(target error) bool {
if t, ok := target.(PermissionsNotAllowedError); ok && len(e) len(t) {
for i := range e {
if e[i] != t[i] {
return false
}
}
return true
}
return false
}

In the meantime I just ditched the second thing altogether and use the simple ErrPermissionNotAllowed. Maybe I come back when I actually work on the UI stuff.

Now writing this it occurs to me that I could do an explicit – second – unit test assertion for array equality and implement my Is and As functions with a type check only and don't care about the exact array. Like that (again, untested):

func (e PermissionsNotAllowedError) Is(target error) bool {
_, ok := target.(PermissionsNotAllowedError)
return ok
}

Yeah, that's probably the way to do it.
_
@lyse do you need to have an explicit Is function? I believe errors.Is has reflect lite and can do the type infer for you. The Is is only really needed if you have a dynamic type. Or are matching a set of types as a single error maybe? The only required one would be Unwrap if your error contained some other base type so that Is/As can reach them in the stack.


As is perfect for your array type because it asserts the matching type out the wrap stack and populates the type for evaluating its contents.
@lyse do you need to have an explicit Is function? I believe errors.Is has reflect lite and can do the type infer for you. The Is is only really needed if you have a dynamic type. Or are matching a set of types as a single error maybe? The only required one would be Unwrap if your error contained some other base type so that Is/As can reach them in the stack.


As is perfect for your array type because it asserts the matching type out the wrap stack and populates the type for evaluating its contents.
So you would have:

type ErrPermissionNotAllowed []Permission
func (perms ErrPermissionNotAllowed) Is(permission Permission) bool {
    for _, p := range perms {
        if p == permission { return true }
    }
    return false
}
var err error = errPermissionNotAllowed{"is-noob"}

if errors.Is(err, ErrPermissionNotAllowed{}) { ... } // user is not allowed

var e ErrPermissionNotAllowed
if errors.As(err, e) && e.Is("a-noob") { ... } // user is not allowed because they are a noob. 
So you would have:

type ErrPermissionNotAllowed []Permission
func (perms ErrPermissionNotAllowed) Is(permission Permission) bool {
    for _, p := range perms {
        if p == permission { return true }
    }
    return false
}
var err error = errPermissionNotAllowed{"is-noob"}

if errors.Is(err, ErrPermissionNotAllowed{}) { ... } // user is not allowed

var e ErrPermissionNotAllowed
if errors.As(err, e) && e.Is("a-noob") { ... } // user is not allowed because they are a noob. 
I suppose to lesson confusion I would rename Is to Because
I suppose to lesson confusion I would rename Is to Because
@xuu Aha, thank you very much! I have to look more into that in the next days.