CAGR is useful because it compresses a beginning value, ending value, and time span into one annualized rate. It is also easy to misuse as if an investment grew at that rate every year. I built the calculator to show both the compact percentage and a reconstructed year-by-year path, making that distinction hard to miss.
The core formula describes an equivalent rate
The computed result normalizes the inputs and clamps the number of years:
const b = Number(beginValue.value) || 0;
const e = Number(endValue.value) || 0;
const Y = Math.min(
Math.max(Math.floor(Number(years.value) || 0), 1),
80
);
if (b <= 0 || e <= 0) return null;
const cagr = Math.pow(e / b, 1 / Y) - 1;
The formula asks: “What constant annual rate would turn the beginning value into the ending value over Y years?” It does not inspect the path between those endpoints. A portfolio could crash and recover and still have the same CAGR as a smooth line.
Total return answers a different question
The source keeps simple growth separate:
const totalReturn = e / b - 1;
const gain = e - b;
totalReturn is the complete change across the period. cagr is that change annualized with compounding. A 21 percent total return over three years is not a 21 percent annual return; the equivalent annual rate is smaller. Showing both prevents a familiar percentage from being read with the wrong time unit.
The yearly table is a reconstructed path
Once CAGR is known, the tool builds illustrative values:
const rows = [];
for (let y = 0; y <= Y; y++) {
const value = b * Math.pow(1 + cagr, y);
rows.push({
year: y,
value,
growth: value - b,
});
}
This table is not historical data. It is the smooth path implied by the equivalent rate. That makes it useful for explaining compounding, but it should not be mistaken for the actual annual returns that produced the endpoint.
The formatter makes the sign explicit:
function fmtPct(val) {
if (val === null || val === undefined || isNaN(val)) return "—";
const pct = val * 100;
const sign = pct > 0 ? "+" : "";
return `${sign}${pct.toFixed(2)}%`;
}
Negative CAGR is valid when the ending value is lower, while zero gets no plus sign. The input guard rejects zero or negative endpoints because the logarithmic intuition behind growth rates becomes misleading for this simple ratio model.
The major limitation is what the formula leaves out: deposits, withdrawals, dividends, taxes, fees, inflation, volatility, and cash-flow timing. If money enters or leaves during the period, a money-weighted or time-weighted return measure may be more appropriate. The 80-year cap is also a display boundary, not a statement about finance.
A smooth chart can be honest if it labels the assumption
The reconstructed rows start at year zero, where value equals the beginning value and growth is zero. Each later row compounds the same equivalent rate again. That makes the table a teaching aid: it shows what “annualized” means without claiming that the account actually followed that line.
There is also a useful relationship to check manually. The final row should equal the ending input apart from floating-point and display rounding, because the rate was derived from that endpoint. gain should equal end - begin, while the cumulative growth in the last row should express the same difference. These simple invariants are valuable when changing a financial component; they catch swapped endpoints and off-by-one year loops quickly.
Negative results deserve the same care as positive ones. If the ending value is lower, fmtPct adds no plus sign and the reconstructed path declines each year. That path is still only an equivalent path. A real investment can have negative years followed by recovery, and CAGR alone cannot tell the story of volatility or drawdown.
If a future version adds cash flows, I would not quietly overload these inputs. I would introduce a separate return method and explain its assumptions, because a single “CAGR” label should not start pretending to be an internal rate of return calculator.
The formatter rounds currency-like values to whole units while leaving the percentage at two decimal places. That is a display choice, not a loss of precision in the calculation. Keeping raw numeric fields in the returned result means another presentation—more decimals, a chart tooltip, or a localized currency symbol—can be added without rewriting the formula.
That split between raw result and formatted text is especially helpful when comparing two scenarios: the UI can show friendly numbers while tests still inspect the unrounded cagr, gain, and row values.
The final result remains a model, not a guarantee, and the input values still need context before anyone makes a decision.
I would also avoid comparing CAGR values with different time windows as if they were automatically equivalent. The exponent normalizes each result to an annual rate, but a two-year result and a twenty-year result can have very different uncertainty, volatility, and practical meaning. The component can calculate both because the formula is compact; interpretation still belongs to the person supplying the beginning and ending values.
That boundary is useful for implementation too. The view does not fetch market prices, infer missing dates, or decorate the result with a prediction. It receives three numbers, calculates a reproducible equivalent rate, and exposes the intermediate rows. Fewer hidden inputs make the output easier to audit, even when the financial story behind those inputs is complicated.
I turned this explanation into a small free tool: CAGR Calculator.
Top comments (0)