Notes › Tech Notes › Factorial page says 0 to 1000 cannot overflow: Number loses exactness at 23! and only overflows at 171!

Factorial page says 0 to 1000 cannot overflow: Number loses exactness at 23! and only overflows at 171!

How that "0 to 1,000 cannot overflow" line made me suspicious

The copy on this page reads "based on BigInt, it computes factorials from 0 to 1,000 and never overflows". The other half of the same sentence is hiding right next to it: Number tops out near 1.7×10^308 (170!), and only larger values need BigInt at all. So I re-checked 23! with the most ordinary code — var r = 1; for (var i = 2; i <= 23; i++) r *= i; — and it returns 25852016738884978212864, while the true value is 25852016738884976640000, a gap of 1572864; the first wrong digit sits at position 17 of what should be a 23-digit number. 23 is inside the range the page advertises, 147 steps earlier than "only at 170 does it break".

Setting the verdict aside for a moment: the page itself does not make this mistake, because it has no Number multiplication path. What does go wrong is the step where we re-check it in Excel or in a script we dash off.

The template has one multiply loop, and from line 111 it is BigInt

factorialBig (lines 108-116) contains a single loop: line 111 is var result = 1n;, line 112 is for (var i = 2n; i <= BigInt(n); i++), line 113 is result *= i;. Across the whole file there is one BigInt seed and zero Number seeds; even 5! is computed as BigInt. The copy's remark that larger values "need BigInt" describes a switch that never takes place.

Overflow begins at 171, exactness ends at 23: the two boundaries are not the same place

Measuring stickWhere it breaksWhat that step really holds
Number exactness23!sits 1572864 above the true value
Number finiteness171!170! is still 7.257415615307994e+306, from 171! it is Infinity
Page BigIntno boundary170! gives 307 digits, 171! gives 310, 1000! gives 2568

One more detail worth holding on to: 170! and 171! both end in 41 zeros (171 contributes no factor 5), yet the digit count climbs from 307 to 310. "It does not overflow" and "the digits are right" are two separate promises; the first one is kept 147 steps longer, and the second is already broken after 22!.

Number exact loses digits from 22 Number finite 171 is Infinity Page BigInt 1000 gives 2568 digits 0 1000

Fig. 1: n runs along the axis, and each bar is the stretch over which that measuring stick can be trusted.

1,000 is an input gate, not a limit of the algorithm

Line 142 reads if (n > 1000). It uses "greater than", so 1,000 itself passes: the raw value of 1,000! is 2,568 digits, it displays as 3,423 characters once the thousands separators are inserted, and the extra 855 commas are formatting only, because the "result digits" field on line 169 reports the length of the raw value. Typing 1,001 is a hard error, the toast reads n 不能超过 1,000, and nothing is truncated or swallowed quietly. Bypass the gate and call the function directly and 1,001! returns 2,571 digits while 2,000! returns 5,736 — you cannot type those into the page, but they are enough to show that 1,000 is only a gate.

249 trailing zeros: no such field exists on the page, so this is a script-side count

Let me fix the scope first: the statistics block on lines 168, 169 and 170 has only three fields, n, result digits, and whether the result is overlong, and grepping the template for 尾随 or 零个数 returns no match. So this number is one I counted myself. I took the zeros at the tail of the BigInt value and tested them against Legendre's formula floor(n/5)+floor(n/25)+floor(n/125)+floor(n/625) for every n from 0 to 1,000, with zero disagreements: 1,000! = 200+40+8+1 = 249 zeros, 500! has 124, 100! has 24, 25! has 6.

The staircase itself has holes. Of the values from 1 to 249 only 201 are reachable and 49 are missing, and the smallest hole is 5: 24! still has four zeros, 25! jumps straight to six, because 25 contributes two 5s at once and 125 contributes three.

5 24 25 100 125 500 1000 1 4 6 24 31 124 249 Trailing zeros gap at the 5 step

Fig. 2: how the count of trailing zeros climbs as n grows (script-side count).

The most hidden failure path: parseInt stops at the first letter

Line 137 is parseInt(nStr, 10). It halts at the first character that is not a digit and raises no error, so the largest-looking input can produce the smallest result:

  • 1e21 parses as 1, the page quietly shows 1! = 1, and the gate on line 142 never fires.
  • 1,000 and 1e5 both give 1 as well; 0x10 stops at the x and gives 0.
  • Decimals truncate toward zero: 5.5 and 5.9 both read as 5 and return 120, with no hint.
  • An empty string, abc, and full-width 20 all fall into the isNaN branch on line 140, which only hides the result area.
  • Typing 1001 is oddly the cleanest case: one clear sentence, n 不能超过 1,000.

Range list: the copy says 100 entries, the code lets 101 through

The hint on line 63 says the list shows at most 100 entries, but the guardrail on line 194 is if (n - m > 100): it compares the difference, not the number of entries. So m=0, n=100 is let through, line 213 prints its own tally as 项数 101, and the list HTML is 18,133 characters; m=1, n=101 and m=50, n=150 are also 101 entries. Turn it around and m=0, n=1000 is refused with the message 范围过大,最多支持 100 entries, so the operation of listing every value from 0! to 1,000! does not exist on the page.

Every timing below is script-side (Node v24.19.0, and the page has no timing field at all): calcSingle(1000) runs end to end in 1.020 ms, and the most expensive step is 900 to 1,000 at 102.485 ms, writing 334,650 characters into the DOM, because line 202 recomputes each i from scratch. Calling the function directly on 1,000, 5,000 and 10,000 takes 0.446, 6.644 and 20.721 ms, so n grows 10 times and the time grows 46 times.

The 5,898 characters of the process display, and not one line cut

Lines 159 to 162 are an unconditional parts.join(' × '), and the only slice in the whole file sits on line 123 stripping a minus sign: the process line for 1,000! is 5,898 characters, the " × " separators number exactly 999, and the innerHTML of the block is 9,460 characters, with hasEllipsis=false at every step. If you want to find fault, look in the copy instead: the process block for 5! is two separate divs in the DOM, line 162 ends at " × 1" and = 120 lives in the div on line 163, so that continuous form never exists on screen.

Q: So where exactly does it go wrong between 0 and 1,000?

Not on the page. It has one BigInt multiplication path, and for every n from 0 to 1,000 it agrees with the formula with zero disagreements. What goes wrong is the re-check: Number stops being exact from 23! and turns into plain Infinity at 171!.

Q: Can I see the number of trailing zeros on the page?

You cannot. The statistics block carries only n, result digits, and whether the result is overlong, three fields (lines 168 to 170), and the overlong threshold is strictly greater than 20 digits: 21! still shows 否 and only 22! shows 是. The 249 was counted by a script.

Q: What if I want the whole column from 0! to 1,000! in one go?

Range mode cannot do it: line 194 refuses on the difference, and the widest span that gets through is 101 entries. If you really want the long list, take it step by step in the single-value box of the factorial calculator, where the largest step runs to 2,568 digits.

If you are collecting factorials on the way to permutations and combinations, read the article on permutations versus combinations first, because computing a raw value before cancelling numerator against denominator is a familiar way to fall over; the same multiply-before-dividing trap shows up in the least common multiple article, where the intermediate value overflows before the result does.

📎 Related articles

A Single CJK Character Is Worth 5 bits: When Crack Time Turns Into Age of the Universe 2026-10-27 The ASCII in text-to-ASCII is really UTF-8: one Han character prints three numbers, and 100% cannot be reversed 2026-10-27 The GCD page prints the zero-remainder row too: step counts, fold order, and the 29-step Fibonacci worst case 2026-10-27 Resizing a GIF Keeps Only the First Frame: a PNG File Named .gif 2026-10-27 A BMP Goes In, a PNG Comes Out: 12342 Bytes Become 281, but the File Is Still Named .bmp 2026-10-27