gastownhall/beads · error

work item %d: %w

Error message

work item %d: %w

What it means

Reconciler.checkSingleItem fetches an individual work item to verify it still exists/is accessible; any error that isn't an explicit 404 (Not Found → Deleted list) or 403 (Forbidden → Denied list) is recorded in result.Errors wrapped as 'work item <id>'. Reconcile continues checking other items and aggregates these errors.

Source

Thrown at internal/ado/reconcile.go:135

	_, err := r.Client.FetchWorkItems(ctx, []int{id})
	if err == nil {
		return // Item exists and is accessible
	}

	idStr := strconv.Itoa(id)

	var apiErr *APIError
	if errors.As(err, &apiErr) {
		switch apiErr.StatusCode {
		case http.StatusNotFound:
			result.Deleted = append(result.Deleted, idStr)
			return
		case http.StatusForbidden:
			result.Denied = append(result.Denied, idStr)
			return
		}
	}
	result.Errors = append(result.Errors, fmt.Errorf("work item %d: %w", id, err))
}

func (r *Reconciler) getInterval(ctx context.Context) int {
	val, err := r.Store.GetConfig(ctx, configReconcileInterval)
	if err != nil || val == "" {
		return DefaultReconcileInterval
	}
	n, err := strconv.Atoi(val)
	if err != nil || n <= 0 {
		return DefaultReconcileInterval
	}
	return n
}

func (r *Reconciler) getCounter(ctx context.Context) int {
	val, err := r.Store.GetConfig(ctx, configSyncsSinceReconcile)
	if err != nil || val == "" {
		return 0

View on GitHub (pinned to 71377f2769)

Solutions

  1. Unwrap the inner error and check its HTTP status; fix auth (rotate PAT) for 401.
  2. Check Azure DevOps service health if the status is 5xx, then re-run reconcile.
  3. Verify ado.org/ado.project configuration still points at the right org/project.
  4. Retry later for transient network errors — reconcile is safe to re-run.

Example fix

// before: every non-404/403 error is just collected
result.Errors = append(result.Errors, fmt.Errorf("work item %d: %w", id, err))
// after: classify auth errors for clearer reporting
var apiErr *APIError
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusUnauthorized {
    result.Errors = append(result.Errors, fmt.Errorf("work item %d: authentication failed, check ado.pat/AZURE_DEVOPS_PAT: %w", id, err))
    return
}
result.Errors = append(result.Errors, fmt.Errorf("work item %d: %w", id, err))
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-validate config/auth before reconcile
if os.Getenv("AZURE_DEVOPS_PAT") == "" && patConfigEmpty {
    return fmt.Errorf("PAT not configured; reconcile would fail on every item")
}

Type guard

func isAuthError(err error) bool {
    var apiErr *APIError
    return errors.As(err, &apiErr) &&
        (apiErr.StatusCode == http.StatusUnauthorized || apiErr.StatusCode == http.StatusForbidden)
}

Try / catch

result := reconciler.Reconcile(ctx)
for _, err := range result.Errors {
    if isAuthError(err) {
        return fmt.Errorf("reconcile aborted: fix credentials: %w", err)
    }
    log.Printf("item-level reconcile issue (continuing): %v", err)
}

Prevention

When it happens

Trigger: GET of the work item during Reconcile fails with anything other than 404/403 — 401 from a bad/expired PAT, 500 from ADO, timeouts, DNS/network errors, or 400 from a malformed work item URL.

Common situations: Expired or revoked Azure DevOps PAT mid-sync; ADO service outage/5xx during reconciliation; corporate proxy breaking TLS; project renamed so the configured project no longer resolves.

Understand the failure class

Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/1e5a73ff64abbb9e. Report an issue: GitHub.