DEV Community

BeGoodTool.com
BeGoodTool.com

Posted on

Why `new Date(garbage) === "Invalid Date"` is always false (a timestamp converter taught me this the hard way)

I built a small Unix timestamp converter — paste in seconds or milliseconds, get a date back, or go the other way. Simple enough that I figured the only real work would be the date math. Then I went to add "tell the user when they typed garbage" and ran straight into a JavaScript gotcha that's been quietly living in more codebases than anyone wants to admit.

The unit dropdown is secretly a multiplier, not a branch

The converter has a unit selector next to the input — "Seconds" or "Milliseconds." The tempting way to wire that up is an if/else: if seconds, do this math; if milliseconds, do that math. That's not what's in the actual component. The <a-select> options are bound directly to the numbers 1 and 1000, and those numbers get used as literal arithmetic operands everywhere downstream:

const unixToNoraml_getOutput = () => {
  let unix = data.unixToNoraml.unix;
  let unit = data.unixToNoraml.unit; // 1 or 1000
  let output = new Date(Math.floor((unix * 1000) / unit));
  data.unixToNoraml.output = output;
};
Enter fullscreen mode Exit fullscreen mode

If unit is 1 (seconds), that's unix * 1000 / 1 — converts seconds to the milliseconds Date wants. If unit is 1000 (milliseconds), it's unix * 1000 / 1000, which cancels out and leaves the value untouched, because it was already in milliseconds. Same one-liner handles both cases with no conditional at all.

The reverse direction does the same trick in reverse — compute the answer in seconds, then multiply by the unit to get either seconds or milliseconds back out:

const normalToUnixOutputFormat = computed(() => {
  if (!data.normalToUnix.output) return "";
  return data.normalToUnix.output * data.normalToUnix.unit;
});
Enter fullscreen mode Exit fullscreen mode

It's a small thing, but reusing the dropdown's raw value as a multiplier instead of introducing a "seconds" | "milliseconds" enum saved a branch in four separate places in the component.

The validation check that doesn't actually validate anything

Here's the one that got me. When you type a date string into the "normal time" field and hit convert, the code tries to reject bad input before doing math on it:

if (new Date(normal) === "Invalid Date") {
  return Swal.fire({
    title: t("timestamp.error"),
    icon: "warning",
    confirmButtonColor: "#1890ff",
  });
}
let output = new Date(normal).getTime() / 1000;
Enter fullscreen mode Exit fullscreen mode

new Date("not a real date") doesn't throw and doesn't return the string "Invalid Date" — it returns an actual Date object whose internal time value is NaN. Calling .toString() on that object prints "Invalid Date", but the object itself is never, ever === to a string, no matter what garbage you feed it. So that guard clause is dead code. It will never fire, for any input, ever.

What actually keeps the tool from displaying literal "NaN" to the user is something else entirely, three lines away in a computed property:

const normalToUnixOutputFormat = computed(() => {
  if (!data.normalToUnix.output) return "";
  return data.normalToUnix.output * data.normalToUnix.unit;
});
Enter fullscreen mode Exit fullscreen mode

NaN / 1000 is still NaN, and !NaN evaluates to true in JavaScript — so the falsy check blanks the output field instead of the intended validation ever running. The tool behaves correctly, but by accident: the real safety net is a coincidental side effect of how NaN interacts with !, not the Swal.fire() warning dialog that was written to handle exactly this case. The correct check would be isNaN(new Date(normal).getTime()), but that's not what's in the file.

The live clock ticks in your local time, not UTC

The page also has a running "current timestamp" display that updates once a second, with pause/continue buttons:

const init_clock = () => {
  if (data.timer) return;
  data.timer = setInterval(() => {
    let now = new Date();
    data.currentUnix = Math.round(now.getTime() / 1000);
    data.currentLocalString = DateTime.fromSeconds(data.currentUnix).toFormat(
      "yyyy/MM/dd HH:mm:ss",
    );
  }, 1000);
};
const pause_clock = () => {
  clearInterval(data.timer);
  data.timer = null;
};
Enter fullscreen mode Exit fullscreen mode

Two things worth calling out. First, setInterval(..., 1000) doesn't guarantee a tick every exact second — it just re-reads Date.now() and rounds each time it fires, so under heavy main-thread load a tick can land late; the displayed number just catches up rather than drifting permanently. Second, DateTime.fromSeconds() from Luxon defaults to the local system time zone when you don't pass one — so the human-readable clock next to the raw Unix number is your machine's local time, not UTC, even though a Unix timestamp is by definition timezone-independent. There's no UTC toggle for it. If you're in Tokyo and I'm in Berlin, we see the same integer but a different clock face next to it.

Limitations, honestly

  • No digit-count auto-detection. I assumed a paste-in converter like this would sniff "10 digits, must be seconds" vs "13 digits, must be milliseconds." It doesn't — the unit is a manual dropdown. Paste a 13-digit millisecond value while "Seconds" is still selected and you silently get a date several thousand years in the future. No warning, no clamp.
  • Everything renders in local time. The seconds-since-epoch value is timezone-agnostic by definition, but every human-readable output on the page — the live clock, the converted date — is displayed in whatever timezone the browser is set to, with no UTC option. If you're debugging a server log recorded in UTC, you have to do the offset math yourself.
  • The "type a date manually" field parses whatever new Date(string) accepts, which is a browser-implementation detail, not a fully spec'd format. It works fine for the YYYY/MM/DD HH:MM:SS shape the placeholder suggests, but it's not the same guarantee you'd get from an explicit parser.
  • The Invalid Date check discussed above never runs. It's harmless here because NaN's falsiness happens to save it, but it means the warning dialog telling you "please enter a valid time" is unreachable code.

I turned the cleaned-up version into a small free tool: Unix Timestamp Converter. No sign-up, works entirely in the browser.


Available in other languages

Top comments (0)