Question
In Go, if a function returns a time.Time, I cannot return nil during an error case because Go reports:
cannot use nil as type time.Time in return argument
What is the zero value of time.Time, and how should it be used when a valid time is not available?
Short Answer
By the end of this page, you will understand why time.Time cannot be nil, what its zero value is, how to return it from functions, and how to check whether a time.Time has been set in real Go programs.
Concept
In Go, every type has a zero value. This is the default value a variable gets when you declare it without explicitly assigning anything.
For example:
int→0string→""bool→false- pointers, slices, maps, functions, interfaces →
nil
But time.Time is not a pointer. It is a struct type from Go's time package. Structs cannot be nil unless you use a pointer to them, such as *time.Time.
So the zero value of time.Time is simply:
time.Time{}
This represents the earliest representable time in Go's time.Time type:
0001-01-01 00:00:00 +0000 UTC
This matters because many Go APIs return zero values together with an error when something goes wrong. If your function returns (time.Time, error), then on failure you usually return:
return time.Time{}, err
This follows a common Go pattern:
- return the zero value for the main result
- return a non-
nilerror
Go code often uses time.Time{} or t.IsZero() to represent “no meaningful time available yet.”
Mental Model
Think of a time.Time value like a printed form with date fields on it.
- A pointer can be
nil, which is like saying: “there is no form at all.” - A struct value like
time.Timealways exists as a full form. - Its zero value is like a blank default form that has not been filled in with a real date.
So when you have a plain time.Time, you cannot say “no form” with nil. You can only use its default blank value:
time.Time{}
If you really need the idea of “no time at all,” then you can use a pointer:
var t *time.Time = nil
But in normal Go code, returning time.Time{} plus an error is usually the simplest and most idiomatic choice.
Syntax and Examples
The core syntax is straightforward.
Zero value of time.Time
var t time.Time
fmt.Println(t)
Because t was declared but not assigned, it gets the zero value automatically.
Explicit zero value
zero := time.Time{}
fmt.Println(zero)
This creates the same zero value explicitly.
Returning zero value from a function
package main
import (
"errors"
"time"
)
func getTimestamp(ok bool) (time.Time, error) {
if !ok {
return time.Time{}, errors.New("timestamp not available")
}
return time.Now(), nil
}
Here:
time.Now()is returned when successfultime.Time{}is returned when there is an error
Checking whether a time is zero
package main
import (
"fmt"
"time"
)
func main() {
var t time.Time
fmt.Println(t.IsZero()) // true
fmt.Println(time.Now().IsZero()) // false
}
The IsZero() method is the safest and clearest way to check whether a time.Time has its zero value.
Step by Step Execution
Consider this example:
package main
import (
"errors"
"fmt"
"time"
)
func parseDeadline(input string) (time.Time, error) {
if input == "" {
return time.Time{}, errors.New("empty deadline")
}
return time.Parse("2006-01-02", input)
}
func main() {
deadline, err := parseDeadline("")
fmt.Println("deadline:", deadline)
fmt.Println("is zero:", deadline.IsZero())
fmt.Println("error:", err)
}
What happens step by step
main()callsparseDeadline("").- Inside
parseDeadline, the input is an empty string. - The condition
input == ""is true. - The function returns:
time.Time{}as the zerotime.Time- an error created with
errors.New(...)
- Back in
main(),deadlinenow holds the zero time value. - Printing
deadlineshows something like:
0001-01-01 00:00:00 +0000 UTC
deadline.IsZero()returnstrue.- The error contains the message
empty deadline.
This shows the standard Go pattern: return the zero value of the main type when the operation fails.
Real World Use Cases
time.Time zero values appear in many practical Go programs.
1. Function results with errors
A function that fetches a timestamp from a database or API may return:
return time.Time{}, err
if the lookup fails.
2. Optional timestamps
You may have fields such as:
CreatedAtUpdatedAtDeletedAtPublishedAt
A zero time.Time can mean the timestamp has not been set yet.
3. Form or config validation
If a date was not provided, a zero time.Time may represent “missing input” until validation decides whether that is allowed.
4. Scheduling systems
A job runner might use zero time to mean:
- never run
- not scheduled yet
- retry time not calculated yet
5. Parsing and transformation pipelines
When converting raw strings into typed data, zero time may temporarily represent an unset or failed value until errors are handled.
Real Codebase Usage
In real Go codebases, developers usually use time.Time in a few common patterns.
Guard clause with zero return
func startTimeFromConfig(cfg string) (time.Time, error) {
if cfg == "" {
return time.Time{}, errors.New("missing config value")
}
return time.Parse(time.RFC3339, cfg)
}
This is a classic guard clause: fail early, return the zero value, and report the error.
Validation with IsZero()
func canPublish(publishedAt time.Time) bool {
return !publishedAt.IsZero()
}
This is clearer than comparing formatted strings or manually checking fields.
Optional field design
Sometimes developers choose between:
time.Timewhen a value should always exist eventually*time.Timewhennilshould explicitly mean “missing” or “not set”
For example:
type User struct {
LastLogin *time.Time
}
Here, nil can mean the user has never logged in.
Error handling pattern
Functions often look like this:
func loadExpiry() (time.Time, error) {
t, err := time.Parse(time.RFC3339, "bad-input")
if err != nil {
return time.Time{}, err
}
return t, nil
}
That pattern is common throughout Go: return the type's zero value on error.
Struct fields and defaults
In configuration structs or model structs, a zero time.Time is often used as the default state until a real timestamp is assigned.
Common Mistakes
Here are common mistakes beginners make with time.Time.
Mistake 1: Returning nil for a non-pointer type
Broken code:
func getTime() (time.Time, error) {
return nil, errors.New("failed")
}
Why it fails:
time.Timeis a struct, not a pointer- only some types can be
nil
Correct version:
func getTime() (time.Time, error) {
return time.Time{}, errors.New("failed")
}
Mistake 2: Comparing to nil
Broken code:
var t time.Time
if t == nil {
fmt.Println("unset")
}
Why it fails:
- a
time.Timevalue can never benil
Correct version:
if t.IsZero() {
fmt.Println("unset")
}
Mistake 3: Using string output to detect zero time
Less reliable approach:
if t.String() == "0001-01-01 00:00:00 +0000 UTC" {
// avoid this
}
Better approach:
if t.IsZero() {
// correct
}
Mistake 4: Using time.Time when *time.Time is more appropriate
Sometimes you need three states:
- no value at all
- value exists
- error happened elsewhere
In that case, a pointer may better represent optional data.
type Event struct {
StartedAt *time.Time
}
Use this only when nil has clear meaning in your design.
Mistake 5: Ignoring the error and trusting the time value
Broken pattern:
t, _ := time.Parse(time.RFC3339, "not-a-date")
fmt.Println(t)
If parsing fails, t will be the zero value and the real problem is hidden.
Better:
t, err := time.Parse(time.RFC3339, "not-a-date")
if err != nil {
fmt.Println("parse failed:", err)
return
}
fmt.Println(t)
Comparisons
Here is how time.Time compares with related choices.
| Concept | Can be nil? | Zero/default value | Best use |
|---|---|---|---|
time.Time | No | time.Time{} | Regular timestamps that always have a value type |
*time.Time | Yes | nil | Optional timestamps where absence matters |
string date like "2024-01-01" | No | "" | Raw input or display text, not ideal for date logic |
time.Time vs *time.Time
var a time.Time // zero value is time.Time{}
var b *time.Time // zero value is nil
Choose time.Time when:
- you want simple value semantics
- a zero value is acceptable as a default
- you prefer idiomatic Go function returns
Choose *time.Time when:
nilhas meaningful business value- you need to distinguish “unset” from “set to a real time”
- you are working with optional JSON/database fields
Zero value vs current time
These are very different:
var t time.Time // zero time
now := time.Now() // current time
A zero value does not mean “now.” It means “default uninitialized time value.”
Cheat Sheet
// Zero value of time.Time
var t time.Time
// Explicit zero value
zero := time.Time{}
// Return zero value on error
return time.Time{}, err
// Check whether a time is zero
if t.IsZero() {
// t has not been set
}
Rules to remember
time.Timeis a struct, so it cannot benil*time.Timeis a pointer, so it can benil- The zero value of
time.Timeistime.Time{} - The printed zero time is usually
0001-01-01 00:00:00 +0000 UTC - Use
IsZero()to test for the zero value - In
(time.Time, error)functions, returntime.Time{}, erron failure
Quick decision guide
- Need a normal timestamp value? Use
time.Time - Need to represent missing time with
nil? Use*time.Time
FAQ
Why can't I return nil for time.Time in Go?
Because time.Time is a struct value, not a pointer. Only certain types such as pointers, slices, maps, interfaces, channels, and functions can be nil.
What is the zero value of time.Time?
The zero value is:
time.Time{}
It prints as 0001-01-01 00:00:00 +0000 UTC.
How do I check if a time.Time is unset?
Use:
t.IsZero()
This is the standard and most readable approach.
Should I use time.Time{} or time.Now() as a default?
Use time.Time{} only when you mean “no real time set yet.” Use time.Now() when you want the current time.
When should I use *time.Time instead of time.Time?
Use *time.Time when nil should represent a truly missing optional value.
Is zero time.Time considered valid?
It is valid as a Go value, but it may not be meaningful in your application's business logic. You should decide whether zero time is allowed in your domain.
What should I return from a function with signature (time.Time, error) when it fails?
Return:
return time.Time{}, err
This matches standard Go error-handling conventions.
Mini Project
Description
Build a small Go program that simulates loading an event start time from user input. This project demonstrates how to return a zero time.Time on failure, how to detect unset times with IsZero(), and how to handle successful and failed parsing clearly.
Goal
Create a program that parses an optional date string and reports whether it received a valid time or a zero time.Time.
Requirements
- Write a function that accepts a date string and returns
(time.Time, error). - Return
time.Time{}when the input is empty or invalid. - Parse valid input using the layout
2006-01-02. - In
main, test both a valid date and an empty string. - Print whether each returned time is zero using
IsZero().
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.