{"record":{"id":"ef7be40ed829723e","repo":"hibiken/asynq","slug":"skip-retry-for-the-task","errorCode":null,"errorMessage":"skip retry for the task","messagePattern":"skip retry for the task","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"processor.go","lineNumber":329,"sourceCode":"\tctx, cancel := context.WithDeadline(context.Background(), l.Deadline())\n\tdefer cancel()\n\terr := p.broker.Done(ctx, msg)\n\tif err != nil {\n\t\terrMsg := fmt.Sprintf(\"Could not remove task id=%s type=%q from %q err: %+v\", msg.ID, msg.Type, base.ActiveKey(msg.Queue), err)\n\t\tp.logger.Warnf(\"%s; Will retry syncing\", errMsg)\n\t\tp.syncRequestCh <- &syncRequest{\n\t\t\tfn: func() error {\n\t\t\t\treturn p.broker.Done(ctx, msg)\n\t\t\t},\n\t\t\terrMsg:   errMsg,\n\t\t\tdeadline: l.Deadline(),\n\t\t}\n\t}\n}\n\n// SkipRetry is used as a return value from Handler.ProcessTask to indicate that\n// the task should not be retried and should be archived instead.\nvar SkipRetry = errors.New(\"skip retry for the task\")\n\n// RevokeTask is used as a return value from Handler.ProcessTask to indicate that\n// the task should not be retried or archived.\nvar RevokeTask = errors.New(\"revoke task\")\n\nfunc (p *processor) handleFailedMessage(ctx context.Context, l *base.Lease, msg *base.TaskMessage, err error) {\n\tif p.errHandler != nil {\n\t\tp.errHandler.HandleError(ctx, NewTaskWithHeaders(msg.Type, msg.Payload, msg.Headers), err)\n\t}\n\tswitch {\n\tcase errors.Is(err, RevokeTask):\n\t\tp.logger.Warnf(\"revoke task id=%s\", msg.ID)\n\t\tp.markAsDone(l, msg)\n\tcase msg.Retried >= msg.Retry || errors.Is(err, SkipRetry):\n\t\tp.logger.Warnf(\"Retry exhausted for task id=%s\", msg.ID)\n\t\tp.archive(l, msg, err)\n\tdefault:\n\t\tp.retry(l, msg, err, p.isFailureFunc(err))","sourceCodeStart":311,"sourceCodeEnd":347,"githubUrl":"https://github.com/hibiken/asynq/blob/d135f1439bee74e989b7f9b41ecd542cc87f024a/processor.go#L311-L347","documentation":"SkipRetry is a sentinel returned by a task Handler to tell asynq the task should NOT be retried. In processor.handleFailedMessage, when errors.Is(err, SkipRetry) matches (or retries are exhausted), the processor archives the task instead of re-scheduling it, so the failure is final.","triggerScenarios":"A Handler.ProcessTask implementation returns SkipRetry (optionally wrapped, e.g. fmt.Errorf(\"...: %w\", SkipRetry)) after a failure that retrying cannot fix, and the processor's handleFailedMessage then archives the task.","commonSituations":"Handlers encountering permanent/input errors (malformed payload, 4xx upstream responses, validation failures) where retries would only burn cycles; developers who return it but then can't find the task in the queue and don't realize it landed in the archived/dead state; wrapping it incorrectly with %v instead of %w, which silently disables the errors.Is check and causes normal retries instead.","solutions":["Ensure the handler returns SkipRetry wrapped with %w so errors.Is detects it (or return the sentinel unwrapped).","If you want the task retained for inspection, know that it goes to the archive — retrieve it via Inspector.ListArchivedTasks rather than expecting it in pending.","For failures that are transient, return a plain error instead of SkipRetry so asynq's retry policy applies.","Implement an ErrorHandler (via asynq.Config ErrorHandler) to log/track tasks that hit SkipRetry."],"exampleFix":"// before\nif resp.StatusCode >= 500 {\n    return fmt.Errorf(\"upstream failed: %v\", asynq.SkipRetry) // wrong wrapping; retries anyway\n}\n// after\nif resp.StatusCode >= 400 && resp.StatusCode < 500 {\n    return fmt.Errorf(\"permanent client error %d: %w\", resp.StatusCode, asynq.SkipRetry) // archived, no retry\n}","handlingStrategy":"try-catch","validationCode":"// validate input before returning errors in the handler\nif !json.Valid(task.Payload()) { return fmt.Errorf(\"invalid payload: %w\", asynq.SkipRetry) }","typeGuard":"func isSkipRetry(err error) bool { return errors.Is(err, asynq.SkipRetry) }","tryCatchPattern":"if err := handler.ProcessTask(ctx, task); err != nil {\n    if errors.Is(err, asynq.SkipRetry) {\n        log.Printf(\"task %s archived (no retry): %v\", task.ID(), err)\n    }\n    return err\n}","preventionTips":["Wrap sentinels with %w, never %v, so errors.Is works.","Return SkipRetry only for permanent errors that retries cannot fix.","Inspect the archive (Inspector.ListArchivedTasks) when tasks vanish from pending.","Register an ErrorHandler to monitor archived tasks in production."],"tags":["go","retry","task-handler","sentinel-error","archive"],"backgroundTag":"retry-exhausted","analyzedSha":"d135f1439bee74e989b7f9b41ecd542cc87f024a","analyzedAt":"2026-09-07T19:02:34.660Z","contentChangedAt":"2026-09-07T19:02:34.660Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}