Skip to main content

ISO Date String Parsing Error Fix

Issue

The application was throwing runtime errors when parsing ISO 8601 date strings:

Uncaught Error: Invalid ISO 8601 date string: 2025-07-13T15:58:57.837Z

This error occurred in multiple components that were using parseDate from @internationalized/date to parse ISO date strings with timezone information.

Root Cause

The parseDate function from @internationalized/date is designed to parse date strings in YYYY-MM-DD format only, not full ISO 8601 strings with time and timezone information.

Problematic Pattern

import { parseDate } from "@internationalized/date"

// ❌ This fails with ISO strings containing time/timezone
const formattedDate = parseDate("2025-07-13T15:58:57.837Z").toString()

The function expects simple date strings like "2025-07-13" but was receiving full ISO 8601 strings like "2025-07-13T15:58:57.837Z".

Solution

1. Created Utility Function

Added formatISODate function to utils/toDate.ts:

/**
* Safely format an ISO date string for display
* @param isoString - ISO 8601 date string (e.g., "2025-07-13T15:58:57.837Z")
* @param locale - Locale for formatting (default: "en-GB")
* @returns Formatted date string or fallback message
*/
export function formatISODate(
isoString: string,
locale: string = "en-GB"
): string {
try {
const date = new Date(isoString)
if (isNaN(date.getTime())) {
console.warn("Invalid ISO date string:", isoString)
return "Invalid date"
}

return date.toLocaleDateString(locale, {
day: "2-digit",
month: "2-digit",
year: "numeric",
})
} catch (error) {
console.warn("Error formatting date:", error)
return "Date not available"
}
}

2. Updated All Problematic Usages

Before:

import { parseDate } from "@internationalized/date"

// ❌ Runtime error with ISO strings
const displayDate = parseDate(candidate.createdAt).toString()

After:

import { formatISODate } from "@utils/toDate"

// ✅ Safely handles ISO strings
const displayDate = formatISODate(candidate.createdAt)

Files Modified

1. Pipeline Page (app/company/pipeline/[jobId]/page.tsx)

  • Issue: parseDate(candidate.createdAt).toString() failing with ISO strings
  • Fix: Replaced with formatISODate(candidate.createdAt)
  • Result: Displays dates in "DD/MM/YYYY" format

2. Invitation Table (app/(shared)/userprofile/invitation/components/InvitationTable.tsx)

  • Issue: parseDate(cellValue as string).toString() for createdAt/updatedAt
  • Fix: Replaced with formatISODate(cellValue as string)
  • Result: Consistent date formatting across invitation table

3. Company Page (app/(shared)/userprofile/company/companyPage.tsx)

  • Issue: parseDate(cellValue as string).toString() for createdAt/updatedAt
  • Fix: Replaced with formatISODate(cellValue as string)
  • Result: Consistent date formatting in company data tables

4. Member Table (app/(shared)/userprofile/members/components/MemberTable.tsx)

  • Issue: parseDate(createdAt).toString() for member creation dates
  • Fix: Replaced with formatISODate(createdAt)
  • Result: Member creation dates display correctly

5. Date Utility (utils/toDate.ts)

  • Addition: New formatISODate function for safe ISO date string formatting
  • Features: Error handling, locale support, fallback messages

Benefits of the Fix

  1. 🛡️ Error Prevention: No more runtime errors from invalid date parsing
  2. 📅 Consistent Formatting: All dates display in DD/MM/YYYY format
  3. 🌍 Locale Support: Easy to change locale for different regions
  4. ⚡ Better Performance: Uses native Date constructor instead of parsing library
  5. 🔧 Error Handling: Graceful fallbacks for invalid dates
  6. 🧪 Maintainability: Centralized date formatting logic

Date Format Comparison

InputOld (parseDate)New (formatISODate)
"2025-07-13T15:58:57.837Z"❌ Runtime Error"13/07/2025"
"2025-07-13""2025-07-13""13/07/2025"
"invalid-date"❌ Runtime Error"Invalid date"
null❌ Runtime Error"Date not available"

Testing

  1. ✅ Pipeline page loads without errors
  2. ✅ Candidate creation dates display correctly
  3. ✅ Invitation table shows formatted dates
  4. ✅ Company page date columns work properly
  5. ✅ Member table creation dates are formatted
  6. ✅ All edge cases handled gracefully

Future Considerations

  • The formatISODate utility can be extended to support different date formats (e.g., relative dates like "2 days ago")
  • Consider adding time formatting options for components that need timestamps
  • The function is locale-aware and can be easily adapted for internationalization

Result

No more ISO date parsing errors
Consistent date formatting across all components
Robust error handling for invalid dates
Improved user experience with readable date formats
Maintainable centralized date formatting logic

TWDatepicker emit-format gotcha (May 2026)

A second class of date bug surfaced in the Create Job form: the Due Date validator was firing "Due Date cannot be later than Start Date" even when the user had clearly picked a Due Date earlier than the Start Date. The screenshot showed Start = May 3, Due = May 2, but the form rejected it.

Root cause: TWDatepicker emits two different formats

TWDatepicker (components/ux/TWDatepicker.tsx) emits two different string shapes depending on the includeTime prop:

includeTimeEmitted formatExampleHow new Date() parses it
false (date-only)"YYYY-MM-DD""2026-05-02"UTC midnight (2026-05-02T00:00:00Z)
true (with time)"YYYY-MM-DDTHH:MM:SS" (LOCAL, no Z)"2026-05-03T00:00:00"Local time (2026-05-03T00:00:00 Europe/Stockholm)

The Create Job form has Start Date with includeTime=true and Due Date with includeTime=false. So the Due Date string is interpreted as UTC and the Start Date string as local time. In timezones east of UTC, new Date("2026-05-02").getUTCDate() and new Date("2026-05-03T00:00:00").getUTCDate() returned the same day-of-month (because LOCAL midnight in Stockholm = previous-day 22:00 UTC), so the comparison flipped the wrong way.

This was not a bug in TWDatepicker (the formats are intentional — date-only events shouldn't carry a fake T00:00:00) but a bug in the validator that assumed both inputs round-tripped through Date consistently.

Solution: lexicographic day-prefix comparison

Both formats start with YYYY-MM-DD. Strip the day prefix, validate it's a real ISO date, and compare strings — no Date constructor anywhere in the comparison.

const dayKey = (value?: string | null): string | null => {
if (!value) return null
const v = value.trim()
if (!v) return null
const datePart = v.includes("T") ? v.split("T")[0] : v.split(/\s+/)[0]
return /^\d{4}-\d{2}-\d{2}$/.test(datePart) ? datePart : null
}

export const computeDueDateError = (startDate, dueDate) => {
const sKey = dayKey(startDate)
const dKey = dayKey(dueDate)
if (!sKey || !dKey) return undefined
return dKey > sKey ? "Due Date cannot be later than Start Date" : undefined
}

dKey > sKey is a plain string comparison, which is correct because YYYY-MM-DD is lexicographically ordered the same as chronologically ordered. No reference frame, no timezone, no surprises.

The fix lives in components/jobs/JobRequirementsForm.tsx and is also exported from there so components/reusable/jobs/CreateJob.tsx imports the same helper — single source of truth for the invariant.

Lessons for future date validators

  1. Never new Date(string) for date-only comparison unless you control both sides of the input format. The same string can mean two different instants depending on whether Z/offset is present.
  2. Lexicographic comparison of YYYY-MM-DD is safe and correct — use it for any same-calendar-day check.
  3. Validate the input format before comparing (the regex in dayKey) so a malformed string fails closed instead of fooling the comparator with an empty / partial split.
  4. One validator, exported from one file — duplicating the logic across the form and the wizard footer was how the original Date-based version drifted in the first place.