apache/answer · warning

badge already awarded

Error message

badge already awarded

What it means

Thrown when a Get on the badge_award table (user_id + badge_id + not-deleted) already returns a row, i.e. this user has already been awarded this badge. The repo refuses to insert a duplicate award. It is an intentional uniqueness guard, not a data-corruption signal.

Source

Thrown at internal/repo/badge_award/badge_award_repo.go:80

		}
		if !exist {
			return nil, fmt.Errorf("badge not exist")
		}

		old := &entity.BadgeAward{
			UserID:         badgeAward.UserID,
			BadgeID:        badgeAward.BadgeID,
			IsBadgeDeleted: entity.IsBadgeNotDeleted,
		}
		if badgeInfo.Single != entity.BadgeSingleAward {
			old.AwardKey = badgeAward.AwardKey
		}
		exist, err = session.Get(old)
		if err != nil {
			return nil, err
		}
		if exist {
			return nil, fmt.Errorf("badge already awarded")
		}

		_, err = session.Insert(badgeAward)
		if err != nil {
			return nil, err
		}

		return session.ID(badgeInfo.ID).Incr("award_count", 1).Update(&entity.Badge{})
	})
	if err != nil {
		return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
	}
	return nil
}

// CheckIsAward check this badge is awarded for this user or not
func (r *badgeAwardRepo) CheckIsAward(ctx context.Context, badgeID, userID, awardKey string, singleOrMulti int8) (
	isAward bool, err error) {

View on GitHub (pinned to 3b9f137061)

Solutions

  1. Check for an existing award before calling, or catch this error and treat it as success (idempotent award).
  2. Make the trigger/caller deduplicate: query badge_award for (user_id, badge_id) first.
  3. If re-award is legitimate business-wise, remove/restore the old award instead of inserting a new one.
  4. Add a unique index on (user_id, badge_id) so the DB enforces the same rule.

Example fix

// before
exist, err = session.Get(old)
if exist {
    return nil, fmt.Errorf("badge already awarded")
}
// after
exist, err = session.Get(old)
if err != nil {
    return nil, err
}
if exist {
    return old, nil // idempotent: return existing award instead of error
}
Defensive patterns

Strategy: validation

Validate before calling

var existing entity.BadgeAward
has, err := db.Where("user_id = ? AND badge_id = ?", userID, badgeID).Get(&existing)
if err == nil && has {
    // already awarded — skip or return existing without inserting
    return &existing, nil
}

Type guard

func alreadyAwarded(awards []entity.BadgeAward, userID, badgeID int64) bool {
    for _, a := range awards {
        if a.UserID == userID && a.BadgeID == badgeID {
            return true
        }
    }
    return false
}

Try / catch

award, err := badgeAwardRepo.Award(ctx, req)
if err != nil {
    if strings.Contains(err.Error(), "badge already awarded") {
        return nil // idempotent success
    }
    return err
}

Prevention

When it happens

Trigger: Calling the badge-award path twice for the same (UserID, BadgeID) pair — e.g. a retry after a slow first response, or an automated trigger plus a manual award.

Common situations: Event handlers firing more than once (re-delivered webhook/queue message); admin manually granting a badge the user already earned; missing idempotency in import scripts.

Related errors


AI-assisted analysis of apache/answer@3b9f137061 (2026-09-05). Data as JSON: /api/errors/7ed378876831f3bb. Report an issue: GitHub.