How to use chrono crate in Rust date and time

The `chrono` crate provides the `NaiveDateTime` and `DateTime` types for handling dates and times, with `DateTime<Tz>` being the standard choice for timezone-aware operations.

When the standard library stops at the clock

You're building a scheduler. A job is set to run at 9:00 AM. You hardcode 9:00. Three months later, daylight saving time hits, and the job runs at 8:00 AM or 10:00 AM depending on the OS. Or you parse a log file and the timestamp 2023-02-29 crashes your parser because you didn't account for leap years. Rust's standard library handles durations and system clocks, but it doesn't know what a month is. It doesn't know that February has 28 days, or that 2024 has 29. You need a crate that speaks calendar. chrono is the standard library for dates and times in Rust.

Trust chrono for the calendar math. The standard library leaves that to you.

The ribbon and the label

Think of time as a long, infinite ribbon. chrono splits this into two concepts. NaiveDateTime is a mark on the ribbon. It says "Year 2023, Month 10, Day 27, Hour 14, Minute 30". It has no context. It doesn't know if that's in New York or Tokyo. It's just a calendar calculation. DateTime<Tz> is that same mark, but wrapped in a timezone. It anchors the mark to a location. When you convert between timezones, you're moving the wrapper, not the mark on the ribbon. The instant in time stays the same; only the label changes.

Keep the ribbon straight. The instant is immutable; the timezone is just a lens.

Minimal example: parsing, now, and formatting

Add chrono to your dependencies. Version 0.4 is the stable release used across the ecosystem.

[dependencies]
chrono = "0.4"

This example shows the core workflow: parse a string, get the current time, and format for display.

use chrono::{DateTime, NaiveDateTime, Utc, Local, TimeZone};

/// Demonstrates basic chrono operations: parsing, current time, and formatting.
fn main() {
    // Parse a string into a naive datetime.
    // parse() uses the FromStr trait, which expects ISO 8601 by default.
    // This returns a Result; unwrap() panics if the string is invalid.
    let naive: NaiveDateTime = "2023-10-27 14:30:00".parse().unwrap();
    println!("Naive: {}", naive);

    // Get the current instant in UTC.
    // Utc::now() returns a DateTime<Utc>, which is timezone-aware.
    // UTC is the anchor for storage and exchange.
    let now_utc: DateTime<Utc> = Utc::now();
    println!("UTC: {}", now_utc);

    // Format the time for display.
    // format() returns a DelayedFormat object, not a String.
    // The formatting happens only when you print it or convert it.
    // This lazy evaluation saves allocations in hot loops.
    println!("Formatted: {}", now_utc.format("%Y-%m-%d %H:%M:%S"));
}

Convention aside: chrono's format() is lazy. It returns a DelayedFormat that computes the string only when printed. This saves allocations. If you need to store the string, call .to_string(). Don't assume format() gives you a String.

Walkthrough: what happens under the hood

When you call parse(), chrono scans the string and builds a NaiveDateTime. It validates the calendar math: is October the 10th month? Yes. Is 30 a valid hour? No. If the string is garbage, parse() returns an Err. You must handle that error, or use unwrap() for quick scripts.

Utc::now() talks to the operating system to get the current instant and wraps it in the UTC timezone. UTC is the universal reference point. It never shifts for daylight saving. When you print a DateTime<Utc>, chrono formats it with a +00:00 or Z suffix to show the offset.

format() uses specifiers like %Y for the four-digit year and %m for the zero-padded month. The syntax matches C's strftime. If you try to store now.format(...) in a String variable, the compiler rejects you with E0308 (mismatched types). format() returns a formatter object. Call .to_string() to materialize the string.

Realistic example: logs, timezones, and expiration

Real code rarely stops at printing. You parse logs, convert timezones for users, and calculate expiration windows. This example shows a function that processes a log entry, converts it to local time, and computes an expiration timestamp.

use chrono::{DateTime, Duration, Utc, Local, TimeZone};

/// Processes a log entry timestamp, converts to local time, and calculates expiration.
/// Assumes the input log is in UTC ISO 8601 format.
fn process_log_entry(log_timestamp: &str) -> Result<(), Box<dyn std::error::Error>> {
    // Parse the log timestamp into a naive datetime.
    // The ? operator propagates the error if parsing fails.
    let naive = log_timestamp.parse::<chrono::NaiveDateTime>()?;

    // Attach the UTC timezone to the naive datetime.
    // and_utc() is the safe way to convert NaiveDateTime to DateTime<Utc>.
    // This anchors the mark to the UTC ribbon.
    let utc_time: DateTime<Utc> = naive.and_utc();

    // Convert to local time for display.
    // with_timezone() keeps the instant but changes the wrapper.
    // The IANA database handles daylight saving shifts automatically.
    let local_time: DateTime<Local> = utc_time.with_timezone(&Local);

    // Calculate expiration: 24 hours from the event time.
    // chrono::Duration handles arithmetic and supports negative values.
    // It is distinct from std::time::Duration, which is always positive.
    let expires_at = utc_time + Duration::hours(24);

    // Print the results.
    // %Z outputs the timezone name, like "UTC" or "PST".
    println!("Event: {}", local_time.format("%Y-%m-%d %H:%M:%S %Z"));
    println!("Expires: {}", expires_at);

    Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    process_log_entry("2023-10-27T14:30:00Z")?;
    Ok(())
}

Convention aside: The Rust community follows a strict rule for time. Store everything in UTC. Display everything in Local. If your database stores DateTime<Local>, you will eventually encounter a bug when the server moves or daylight saving changes. Normalize to UTC at the boundary.

Pitfalls and compiler errors

Mixing NaiveDateTime and DateTime is the most common trap. NaiveDateTime has no timezone. If you store a NaiveDateTime in a database and read it back, you lose the context. You might assume it's UTC, but the app treats it as Local. The result is off by hours. Always attach a timezone immediately after parsing if the source has a known timezone. Use and_utc() or and_local_timezone().

Never let a NaiveDateTime survive past the parsing step. Attach a timezone or drop it.

Another pitfall is chrono::Duration versus std::time::Duration. The standard library duration is always positive and represents a span of time. chrono::Duration can be negative and represents a change in time. You can subtract two DateTime values to get a chrono::Duration. This is useful for calculating elapsed time. Be careful: chrono::Duration is based on nanoseconds. If you convert a large duration to seconds, you might overflow. Use num_days() or num_hours() for large spans.

If you try to assign the result of parse() directly to a NaiveDateTime variable without handling the error, the compiler rejects you with E0308 (mismatched types). parse() returns a Result. You must unwrap it or propagate the error. If you forget the ? or unwrap(), the type system catches the mistake before runtime.

Timezone conversions can also surprise you. When you convert DateTime<Utc> to DateTime<Local>, chrono consults the IANA timezone database. If you are in a timezone that observes DST, the offset changes. chrono handles this automatically. However, if you create a FixedOffset manually, you are responsible for the offset. If you hardcode FixedOffset::east_opt(9 * 3600) for Tokyo, you will be wrong during DST if Tokyo ever adopted it. Prefer Local or named timezones over fixed offsets unless you have a specific reason.

Decision: when to use what

Use NaiveDateTime when you are doing pure calendar math that doesn't depend on location, like calculating the difference between two dates in the same timezone or storing a birthday where the timezone is irrelevant.

Use DateTime<Utc> when you are storing timestamps in a database or exchanging data over a network. UTC is the universal reference point. It avoids ambiguity and daylight saving shifts.

Use DateTime<Local> when you are displaying time to a user on their device. The local timezone reflects the user's environment and handles daylight saving automatically.

Use DateTime<FixedOffset> when you need to work with a specific offset that isn't the system local time, such as a hardcoded offset from a configuration file or a timezone that doesn't observe daylight saving.

Use chrono::Duration for arithmetic involving hours, minutes, and seconds. It handles the conversion to nanoseconds internally and prevents overflow errors that can occur with raw integer math.

Pick the right type for the job. UTC for storage, Local for display, Naive for math that doesn't care where you are.

Where to go next