Carbon Date Handling Tips for Laravel Developers
Carbon is bundled with every Laravel app, and Eloquent automatically casts your created_at and updated_at columns to Carbon instances. Most developers use maybe ten percent of what it offers. These tips cover the parts that save real time and prevent real bugs.
Cast Your Own Columns to Carbon
Eloquent only auto-casts timestamps unless you tell it otherwise. Add your date columns to the casts() method so they come back as Carbon instances instead of raw strings:
protected function casts(): array
{
return [
'published_at' => 'datetime',
'trial_ends_at' => 'datetime',
'birth_date' => 'date',
];
}
Now $post->published_at->diffForHumans() just works, instead of giving you a Call to a member function on string error.
Use Human-Readable Comparisons
Carbon's named comparison methods read like English and avoid timezone mistakes:
$post->published_at->isPast();
$subscription->trial_ends_at->isFuture();
$event->starts_at->isToday();
$invoice->due_date->isBetween(now(), now()->addDays(7));
Store UTC, Display Local
The golden rule of dates: store everything in UTC and convert to the user's timezone only when displaying. Keep APP_TIMEZONE=UTC in your config and convert at the edges:
// Database holds UTC. Convert for display:
$localTime = $appointment->starts_at
->setTimezone($user->timezone)
->format('M j, Y g:i A');
Never store local times in the database. The moment a user travels or daylight saving shifts, ambiguous local timestamps become impossible to reason about.
Do Math Without Mutating
Older Carbon code mutates the original instance, which causes spooky action-at-a-distance bugs. The modern, safe approach uses methods that return new instances:
$start = now();
$end = $start->copy()->addDays(30); // copy() protects $start
// Or use the immutable variant everywhere
$start = CarbonImmutable::now();
$end = $start->addDays(30); // $start is untouched
If you set 'date' => CarbonImmutable::class casting, your model dates become immutable by default and a whole class of bugs disappears.
Format for Humans and for APIs
diffForHumans() gives you the "3 hours ago" strings users expect:
$comment->created_at->diffForHumans(); // "2 hours ago"
$trial->ends_at->diffForHumans(); // "in 5 days"
$event->starts_at->diffForHumans(now(), true); // "5 days" (no "ago"/"in")
For APIs, always serialize to ISO 8601 so clients can parse unambiguously:
$post->published_at->toIso8601String(); // "2026-06-25T14:30:00+00:00"
Freeze Time in Your Tests
The biggest Carbon superpower for testing: you can freeze time so date-dependent logic is deterministic. No more flaky tests that fail at midnight:
public function test_trial_expires_after_14_days(): void
{
$this->travelTo(now()->startOfDay());
$user = User::factory()->create(['trial_ends_at' => now()->addDays(14)]);
$this->travel(15)->days();
$this->assertTrue($user->fresh()->trial_ends_at->isPast());
}
travelTo() jumps to a specific moment, travel() moves relative to it, and time stays frozen until the test ends. This single feature turns brittle, time-sensitive tests into rock-solid ones.
Carbon rewards a little investment. Cast your columns, store UTC, prefer immutable instances, and freeze time in tests — do those four things and dates stop being a source of late-night production incidents.