go-sql-driver/mysql · error

year is not in the range [1, 9999]: %s

Error message

year is not in the range [1, 9999]: %s

What it means

Returned by appendDateTime (utils.go:278) when serializing a time.Time to send to MySQL and the year is outside [1,9999]. MySQL's DATE/DATETIME types cannot represent year 0 or years >= 10000, so the driver refuses to format them. It is raised while building interpolated query arguments (connection.go:380) and binary protocol values (packets.go:1211).

Source

Thrown at utils.go:278

			int(data[6]),                              // seconds
			int(binary.LittleEndian.Uint32(data[7:11]))*1000, // nanoseconds
			loc,
		), nil
	}
	return nil, fmt.Errorf("invalid DATETIME packet length %d", num)
}

func appendDateTime(buf []byte, t time.Time, timeTruncate time.Duration) ([]byte, error) {
	if timeTruncate > 0 {
		t = t.Truncate(timeTruncate)
	}

	year, month, day := t.Date()
	hour, min, sec := t.Clock()
	nsec := t.Nanosecond()

	if year < 1 || year > 9999 {
		return buf, errors.New("year is not in the range [1, 9999]: " + strconv.Itoa(year)) // use errors.New instead of fmt.Errorf to avoid year escape to heap
	}
	year100 := year / 100
	year1 := year % 100

	var localBuf [len("2006-01-02T15:04:05.999999999")]byte // does not escape
	localBuf[0], localBuf[1], localBuf[2], localBuf[3] = digits10[year100], digits01[year100], digits10[year1], digits01[year1]
	localBuf[4] = '-'
	localBuf[5], localBuf[6] = digits10[month], digits01[month]
	localBuf[7] = '-'
	localBuf[8], localBuf[9] = digits10[day], digits01[day]

	if hour == 0 && min == 0 && sec == 0 && nsec == 0 {
		return append(buf, localBuf[:10]...), nil
	}

	localBuf[10] = ' '
	localBuf[11], localBuf[12] = digits10[hour], digits01[hour]
	localBuf[13] = ':'

View on GitHub (pinned to 03d76c7e07)

Solutions

  1. Send NULL for absent dates using sql.NullTime{Valid:false} or a nil *time.Time instead of time.Time{}.
  2. Clamp/validate the year to [1,9999] before binding: reject or truncate out-of-range values.
  3. Fix the upstream computation producing year 0 or year >= 10000.
  4. For genuine out-of-range dates, store as a string column and format yourself.

Example fix

// before: zero time.Time (year 1) intended as 'no date'
db.Exec("INSERT INTO t(d) VALUES(?)", time.Time{})
// -> "year is not in the range [1, 9999]: 1" on some paths / far-future overflow

// after: express absence as NULL
var d sql.NullTime // Valid: false -> NULL
db.Exec("INSERT INTO t(d) VALUES(?)", d)
Defensive patterns

Strategy: validation

Validate before calling

// Validate a time.Time before binding it as a MySQL temporal value.
func validForMySQL(t time.Time) bool {
    y := t.Year()
    return y >= 1 && y <= 9999
}

Type guard

func isYearOutOfRange(err error) bool {
    return err != nil && strings.HasPrefix(err.Error(), "year is not in the range [1, 9999]")
}

Try / catch

if !validForMySQL(t) {
    // send NULL instead, or clamp/fix the value before binding
    arg := sql.NullTime{}
    db.ExecContext(ctx, "INSERT INTO t(d) VALUES(?)", arg)
}

Prevention

When it happens

Trigger: Binding a time.Time whose year is 0 (a non-zero-check zero value, or year 0001 which maps to Go year 1 boundary), or a far-future date (year >= 10000) as a query parameter that gets formatted via appendDateTime.

Common situations: Passing the zero time.Time (year 1) intending NULL but not using sql.NullTime; date arithmetic overflow; mock/test data with absurd years; a computed expiry date rolling past 9999.

Related errors


AI-assisted analysis of go-sql-driver/mysql@03d76c7e07 (2026-08-07). Data as JSON: /api/errors/f2771d03901b89f6. Report an issue: GitHub.