go-sql-driver/mysql · error

invalid time bytes: %s

Error message

invalid time bytes: %s

What it means

Thrown by parseDateTime (utils.go:179) — the default branch of the length switch. It fires when the textual datetime byte length is NOT one of {10, 19, 21, 22, 23, 24, 25, 26} (the only legal MySQL DATE/DATETIME text lengths). The offending bytes are interpolated into the message, which is useful for diagnosing what the server actually sent.

Source

Thrown at utils.go:179

		sec, err := parseByte2Digits(b[17], b[18])
		if err != nil {
			return time.Time{}, err
		}
		if len(b) == 19 {
			return time.Date(year, month, day, hour, min, sec, 0, loc), nil
		}

		if b[19] != '.' {
			return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[19])
		}
		nsec, err := parseByteNanoSec(b[20:])
		if err != nil {
			return time.Time{}, err
		}
		return time.Date(year, month, day, hour, min, sec, nsec, loc), nil
	default:
		return time.Time{}, fmt.Errorf("invalid time bytes: %s", b)
	}
}

func parseByteYear(b []byte) (int, error) {
	year, n := 0, 1000
	for i := range 4 {
		v, err := bToi(b[i])
		if err != nil {
			return 0, err
		}
		year += v * n
		n /= 10
	}
	return year, nil
}

func parseByte2Digits(b1, b2 byte) (int, error) {
	d1, err := bToi(b1)

View on GitHub (pinned to c426bd9379)

Solutions

  1. Look at the message: the %s shows the exact bytes. Compare against expected 'YYYY-MM-DD[ HH:MM:SS[.ffffff]]'.
  2. If the format is legitimately different, SELECT the column CAST AS CHAR and parse with the matching time.Parse layout client-side.
  3. Check the column type with SHOW COLUMNS / DESCRIBE — scan string columns as string, not time.Time.
  4. Test directly against MySQL (bypassing proxies) to confirm the server itself emits the odd length.

Example fix

// before: column is actually a VARCHAR holding '20200101'
var t time.Time
err := db.QueryRow("SELECT d FROM t").Scan(&t)

// after
var raw string
if err := db.QueryRow("SELECT CAST(d AS CHAR) FROM t").Scan(&raw); err == nil {
    t, err = time.ParseInLocation("20060102", raw, time.Local)
}
Defensive patterns

Strategy: validation

Validate before calling

// before scanning, confirm the textual length class is one MySQL produces
var raw string
if err := db.QueryRow("SELECT CAST(d AS CHAR) FROM t WHERE id=?", id).Scan(&raw); err != nil {
    return err
}
switch len(raw) {
case 10, 19, 21, 22, 23, 24, 25, 26:
    t, err := time.ParseInLocation("2006-01-02 15:04:05.999999", raw, time.Local)
    _ = t
default:
    return fmt.Errorf("unexpected datetime length %d for %q", len(raw), raw)
}

Try / catch

var t time.Time
if err := rows.Scan(&t); err != nil {
    if strings.Contains(err.Error(), "invalid time bytes") {
        // length is unsupported; read as string and handle/log
    }
}

Prevention

When it happens

Trigger: Scanning a DATE/DATETIME/TIMESTAMP column whose textual wire value has an unexpected length — e.g. an 8-byte 'YYYYMMDD', a 17-byte value, or trailing whitespace/garbage that changes the length class.

Common situations: Server returns a date in a compact 'YYYYMMDD' format (some compatibility modes / non-MySQL servers); a column declared DATETIME but holding a NULL-ish or zero-padded oddity; a proxy stripping or adding bytes; SQL_MODE NO_ZERO_DATE or similar producing unusual representations; a bug where a VARCHAR value reaches a time.Time scan.

Related errors


AI-assisted analysis of go-sql-driver/mysql@c426bd9379 (2026-08-04). Data as JSON: /data/errors/80f0e21011499bb2.json. Report an issue: GitHub.