Jguer/yay · error
expected number, got
Error message
expected number, got %s
What it means
assign maps a Lua value onto a reflect.Int/Int64 struct field and requires an LNumber. Lua has a single numeric type, but anything that is not a number (string, bool, table, nil) is rejected with an error naming the actual type. The caller wraps it with the yay.opt.<key> prefix.
Solutions
- Remove quotes so the value is a Lua number: yay.opt.retries = 3
- Convert deliberately with tonumber(...): yay.opt.retries = tonumber(os.getenv("RETRIES"))
- If the field actually needs a duration/string, change the Go field type and tag instead
Example fix
// before (init.lua) yay.opt.retries = "3" // after yay.opt.retries = 3
Defensive patterns
Strategy: validation
Validate before calling
-- init.lua local n = tonumber(yay_opt_retries_raw) assert(n ~= nil, "retries must be numeric") yay.opt.retries = n
Try / catch
if err != nil && strings.Contains(err.Error(), "expected number") {
fixNumberOptionInInitLua()
} Prevention
- Write numeric options unquoted
- Use tonumber() for values derived from env or strings
- Keep durations as plain int fields (e.g. seconds)
When it happens
Trigger: An init.lua option bound to an int field is set to a quoted string or other non-number, e.g. yay.opt.retries = "3" or yay.opt.timeout = nil.
Common situations: Quoting numbers out of habit from config formats where everything is a string; Lua arithmetic returning nil due to a missing variable; hex or formatted strings ('0x10', '5s').
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
AI-assisted analysis of Jguer/yay@328f4b4939 (2026-09-07).
Data as JSON: /api/errors/0d8bd0d16a9037b5.
Report an issue: GitHub.
Appendix: source
Thrown at pkg/settings/lua/lua.go:177
switch field.Kind() {
case reflect.String:
s, ok := val.(lua.LString)
if !ok {
return fmt.Errorf("expected string, got %s", val.Type())
}
field.SetString(string(s))
case reflect.Bool:
b, ok := val.(lua.LBool)
if !ok {
return fmt.Errorf("expected boolean, got %s", val.Type())
}
field.SetBool(bool(b))
case reflect.Int, reflect.Int64:
n, ok := val.(lua.LNumber)
if !ok {
return fmt.Errorf("expected number, got %s", val.Type())
}
field.SetInt(int64(n))
case reflect.Slice:
return assignStructSlice(field, val)
default:
return fmt.Errorf("unsupported field kind %s", field.Kind())
}
return nil
}
// assignStructSlice fills a []Struct field from a Lua table keyed by name, e.g.
//
// { ["core"] = { url = "..." }, ["extra"] = { url = "..." } }
//
// Each entry becomes one struct: the table key populates the element's
// lua:"name" field and the sub-table populates the remaining fields. EntriesView on GitHub (pinned to 328f4b4939)