I came across a fascinating bug the other day. Some code that involved parsing dates from incoming text had passed testing, but when running on a live server it failed. But not consistently - some dates continued to parse fine, but others would cause errors. The answer was... not what I expected. So maybe some of you might want to be aware of this too:
url copied!
Some code was running which received incoming text that included a date. The date needed to be parsed out of the text and turned back into a proper
DateTime
object for further processing. And the source included something like:
if(DateTime.TryParse(dateString, out DateTime dt))
{
// code that processed the date
}
else
{
throw new InvalidDataException($"Could not parse '{dateString}' as a date");
}
When running this locally on some test data (Windows 11, up-to-date, .Net 10 runtime) it worked fine, and all the test cases were parsed correctly. An example of the dates being parsed looked like:
1 Feb. 2019 23 Apr. 2023 9 May 2022 5 Sept. 2019 7 Sept. 2019 29 Dec. 2021
But when this code was deployed to a production server, the logs started to fill up with failures where the dates had not been parsed.
url copied!
The first thought was "is the server not using the same locale?" but, getting the server code and the local code to output that information showed they were both using
en-GB
and hence that wasn't going to be the issue.
And the second thought was "is the test data invalid" - but looking at the logs, the dates all followed the same format above. Using the same data locally worked fine.
But after some head-scratching, the list of errors reported gives a clue as to what was up - but it might not be obvious: If you looked at only the failing dates, they all happened to be in September. Day and year didn't seem to matter and any September date would fail on the server. But all the other months parsed fine.
So why is September odd? Date parsing is just date parsing, right? It should just work?
Well the answer comes from a bit of Locales in Windows you might not have noticed before: Each locale object contains a list of the names for months - in both full and abbreviated forms. So you can run:
var abbreviateMonths = Thread.CurrentThread.CurrentCulture.DateTimeFormat.AbbreviatedMonthNames;
And you'll have a list of the abbreviations that the code will use both for formatting (if you use
MMM
in a format string) and for parsing text back to dates. Though on a server accessed by RDP, dropping arbitrary bits of C# isn't easy, so PowerShell is quicker for a bit of debugging here. Translating that to
[System.Threading.Thread]::CurrentThread.CurrentCulture.DateTimeFormat.AbbreviatedMonthNames
gets the list, and comparing the local machine's output with the server's:
| Local code | Server code |
|---|---|
| Jan | Jan |
| Feb | Feb |
| Mar | Mar |
| Apr | Apr |
| May | May |
| Jun | Jun |
| Jul | Jul |
| Aug | Aug |
| Sept | Sep |
| Oct | Oct |
| Nov | Nov |
| Dec | Dec |
Between the server's OS and the local machine's OS this data had changed! September has a different abbreviated form, so the parsing that relies on this can fail...
So why is this? Well after some googling it turns out that back in 2021 the ISO committee that owns locales and their related data decided to change from
Sep
to
Sept
as the official English abbreviation. And checking ties up here - the local code was running on Windows 11 and the server was running Windows Server 2019. The server O/S that was failing pre-dated this change.
I guess there are compatibility reasons why subsequent update patches didn't change this on the old server O/S - I guess either way it causes someone some frustration. And they picked "make different Windows versions incompatible" over "break code that was written for the old O/S.
url copied!
Well the hacky solution is to look at the
AbbreviatedMonthNames
list and detect which form the machine expects, and transform any September dates to that form before parsing.
The fancier version is that it is possible to make custom locales at runtime, and keep one hanging around for this situation. You can take the
en-GB
locale and modify it if it's necessary:
var septemberIndex = 8;
var culture = CultureInfo.CreateSpecificCulture("en-GB");
if(culture.DateTimeFormat.AbbreviatedMonthNames[septemberIndex] == "Sep")
{
var names = (string[])culture.DateTimeFormat.AbbreviatedMonthNames.Clone();
culture.DateTimeFormat.AbbreviatedMonthNames.CopyTo(names, 0);
names[septemberIndex] = "Sept";
culture.DateTimeFormat.AbbreviatedMonthNames = names;
culture.DateTimeFormat.AbbreviatedMonthGenitiveNames = names;
}
And then you can use the
DateTime.Parse(dateString, culture)
to get your "right" date parsing behaviour on both variations of the operating system. But just do this culture-modification once and keep it around, to avoid unnecessary heap churn.
url copied!
While "my code is broken on Windows Server 2019" is perhaps a bit of an edge case here in 2026, there's a whole class of issues (and debugging approaches) to consider here. It's easy to assume that standards as universal as calendars are pretty static these days. So remembering that these immutable-feeling things might have changed, and hence looking into differences is important.
Other changes like this may exist in Windows with more recent releases. And maybe the future will bring more. So keep your wits about you when you're deploying code developed on a different O/S to the one that will eventually run on.
↑ Back to top