How to parse and format dates with chrono
Short answer: parse with DateTime::parse_from_rfc3339, format with the .format("...") method and its %-specifiers:
[dependencies]chrono = "0.4"use chrono::DateTime;
fn main() { let raw = "2026-10-28T14:30:00+08:00"; let dt = DateTime::parse_from_rfc3339(raw).expect("invalid timestamp"); println!("{dt}"); println!("{}", dt.format("%Y-%m-%d %H:%M:%S %z"));}cargo run -q2026-10-28 14:30:00 +08:002026-10-28 14:30:00 +0800The two lines are the same instant in two spellings: {dt} prints the Display form (offset with a colon), the %z specifier prints the compact one. From a fresh project — cargo new date-demo, paste the dependency into Cargo.toml, then replace src/main.rs with each block below.
The type is DateTime<FixedOffset>, not a string
Section titled “The type is DateTime<FixedOffset>, not a string”parse_from_rfc3339 returns a DateTime<FixedOffset>: a point in time plus the UTC offset that came out of the input (+08:00 here). That offset is data, not decoration — it is what makes the value comparable with any other DateTime and convertible back with to_rfc3339(). The standard library has nothing like this: SystemTime from the previous article is an opaque instant with no calendar, and a calendar with timezones is exactly what you are paying the dependency for.
One caveat on .format(...): it does not return a String. It returns a borrowing view that implements Display — perfect for println!, but if you want to own it, call .to_string() on the result.
The timezone you forgot
Section titled “The timezone you forgot”Not every timestamp string carries an offset. NaiveDateTime::parse_from_str takes a format string and parses exactly what you give it — no more:
use chrono::{FixedOffset, NaiveDateTime, TimeZone};
fn main() { let naive = NaiveDateTime::parse_from_str("2026-10-28 14:30:00", "%Y-%m-%d %H:%M:%S").unwrap(); let shanghai = FixedOffset::east_opt(8 * 3600).unwrap().from_local_datetime(&naive).unwrap(); let utc = FixedOffset::east_opt(0).unwrap().from_local_datetime(&naive).unwrap(); println!("{naive}"); println!("{}", shanghai.to_rfc3339()); println!("{}", utc.to_rfc3339()); println!("same instant: {}", shanghai == utc);}2026-10-28 14:30:002026-10-28T14:30:00+08:002026-10-28T14:30:00+00:00same instant: falseNaive means what it says: this value knows the wall-clock digits and nothing else. It does not know where it happened, so it does not name a real instant — the same string describes 8 o’clock in Shanghai and 8 o’clock in UTC, and both parse to an equal NaiveDateTime that means two moments eight hours apart. The trap is that nothing warns you at parse time; it surfaces later, when two systems compare timestamps and disagree. from_local_datetime is the moment you commit to an answer: attach the offset you know the string was written in, and from then on every value is a real point in time.
The format specifiers
Section titled “The format specifiers”.format speaks chrono’s own dialect — mostly strftime-compatible, and completely separate from the {} mini-language that println! and format! use for numbers and alignment:
use chrono::DateTime;
fn main() { let dt = DateTime::parse_from_rfc3339("2026-10-28T14:30:00+08:00").unwrap(); println!("{}", dt.format("%Y-%m-%d")); println!("{}", dt.format("%H:%M:%S")); println!("{}", dt.format("%A, %d %B %Y")); println!("{}", dt.format("%s")); println!("{}", dt.to_rfc2822());}2026-10-2814:30:00Wednesday, 28 October 20261793169000Wed, 28 Oct 2026 14:30:00 +0800%s is the Unix timestamp — the same number SystemTime gives you, recovered from the parsed date. to_rfc2822() and to_rfc3339() are ready-made formats for email headers and APIs, so the common cases never need a specifier at all. The same specifiers work in both directions: parse_from_str takes them on the way in, format on the way out.
When parsing fails
Section titled “When parsing fails”Both parse functions return a Result, and mismatched input is a normal error, not a panic:
use chrono::DateTime;
fn main() { match DateTime::parse_from_rfc3339("2026-10-28 14:30:00") { Ok(dt) => println!("{dt}"), Err(e) => println!("rejected: {e}"), }}rejected: premature end of inputThat space is the whole story: RFC 3339 wants T between the date and the time, so the parser stopped where the input stopped matching and reports what it ran out of. If the string came from a database or a log line with a space in the middle, either normalize it to T or switch to NaiveDateTime::parse_from_str with a format that matches it — and then deal with the offset deliberately, as above.
- The previous article covered
SystemTime— the Unix timestamp%srecovers above, and the standard library’s only clock. - Attaching
Localtime and handling DST is its own article later in this chapter. - Working with dates and time — the rest of the chapter.