usememos/memos · error

contains argument must be a string

Error message

contains argument must be a string

What it means

The argument to contains() in a comprehension predicate must be a compile-time string constant. A constant of another type triggers this error; non-constant arguments surface as the wrapped 'contains argument must be a constant string'.

Source

Thrown at internal/filter/parser.go:1016

// buildContainsPredicate extracts the pattern from t.contains("substring").
func buildContainsPredicate(call *exprv1.Expr_Call, iterVar string) (PredicateExpr, error) {
	if target := call.Target.GetIdentExpr(); target == nil || target.GetName() != iterVar {
		return nil, errors.Errorf("contains target must be the iteration variable %q", iterVar)
	}

	if len(call.Args) != 1 {
		return nil, errors.New("contains expects exactly one argument")
	}

	substring, err := getConstValue(call.Args[0])
	if err != nil {
		return nil, errors.Wrap(err, "contains argument must be a constant string")
	}

	substringStr, ok := substring.(string)
	if !ok {
		return nil, errors.New("contains argument must be a string")
	}

	return &ContainsPredicate{Substring: substringStr}, nil
}

View on GitHub (pinned to 14d757ce1f)

Solutions

  1. Quote the substring: tag.exists(t, t.contains("work"))
  2. Use string literals only; bind dynamic values before the filter or use == with a constant

Example fix

// before
`tag.exists(t, t.contains(42))`

// after
`tag.exists(t, t.contains("work"))`
Defensive patterns

Strategy: validation

Validate before calling

var badContains = regexp.MustCompile(`contains\s*\(\s*[^"']`)

func validateContains(expr string) error {
    if badContains.MatchString(expr) {
        return errors.New("contains needs a quoted string substring")
    }
    return nil
}

Try / catch

if err := r.Render(cond); err != nil {
    if strings.Contains(err.Error(), "contains argument must be a string") {
        return userErr("quote the contains substring, e.g. t.contains(\"work\")")
    }
    return err
}

Prevention

When it happens

Trigger: tag.exists(t, t.contains(42)) or tag.all(t, t.contains(dyn_value)) with a non-string literal — comparing the iterated tag against a numeric/boolean literal.

Common situations: Unquoted substrings; users assuming contains accepts any scalar type.

Related errors


AI-assisted analysis of usememos/memos@14d757ce1f (2026-08-15). Data as JSON: /api/errors/0df8b3fc8b5a069f. Report an issue: GitHub.