error TS2366: Function lacks ending return statement and return type does not include 'undefined'. is reported when a function's declared return type (from @returns) does not include undefined and the checker can find a way to reach the closing brace without a return. Falling off the end returns undefined in JavaScript, so the declared type would be wrong on that path.
The surprising case is a chain of if statements that covers every value of a union: the checker does not treat an if chain as exhaustive, so it still sees a path past the last if. A switch over the same cases is treated as exhaustive.
At a glance
| Code | TS2366 |
|---|---|
| Message | Function lacks ending return statement and return type does not include 'undefined'. |
| TypeScript's category | error |
| Listed under | Types do not match |
| Turned on by | strictNullChecks, strict |
| In .js files | Reported when the file is checked: checkJs in jsconfig.json, or // @ts-check at the top of the file |
The message is TypeScript's own text from its diagnostic messages file (Apache-2.0).
What it looks like
/** @param {"small" | "medium" | "large"} size @returns {number} */error TS2366: Function lacks ending return statement and return type does not include 'undefined'.
export function shippingFor(size) {
if (size === "small") return 4;
if (size === "medium") return 6;
if (size === "large") return 9;
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} What causes it
- A branch was forgotten: an
else, a default value, athrowfor impossible input. - Every case is handled by
ifstatements, but the checker does not count anifchain as covering the union, so the end of the function is still reachable. strictNullChecksis on (it is part ofstrict); without itundefinedfits every type and this error is not reported.
How to fix it
Use a switch, or make the last case the final return
A switch whose cases cover the whole union lets the checker see that the end is unreachable. In an if chain, drop the last condition and return unconditionally, or end with throw new Error(...) for values that should never arrive.
/**
* @param {"small" | "medium" | "large"} size
* @returns {number}
*/
export function shippingFor(size) {
switch (size) {
case "small":
return 4;
case "medium":
return 6;
case "large":
return 9;
}
}
/**
* @param {"small" | "medium" | "large"} size
* @returns {number}
*/
export function shippingForBox(size) {
if (size === "small") return 4;
if (size === "medium") return 6;
return 9;
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Or include undefined in the return type
If falling through is intended, put it in the type: @returns {number | undefined}. Callers then have to handle the missing value.
TS2366 and noImplicitReturns
TS2366 is about the declared return type. The separate option noImplicitReturns reports a function where some paths return a value and others do not, even without a @returns tag (TS7030).
Gotchas
- In a
.jsfile the error is reported on the type in the@returnstag, not on the function name. - If no path returns anything at all, the message is TS2355 instead.
Sources
- Narrowing: Exhaustiveness checking · TypeScript handbook
- TSConfig Reference: strictNullChecks · TypeScript
- diagnosticMessages.json (TypeScript 5.9.3) · TypeScript on GitHub
Last reviewed 20 October 2026.