This TypeScript compiler error occurs when a function with an explicit return type does not return a value from all code paths. The compiler detects that some execution branches may reach the end of the function without returning a value, which violates the declared return type.
TypeScript performs comprehensive code path analysis to ensure type safety. When you declare a function with a specific return type, TypeScript expects every possible execution path through that function to return a value matching that type. Error TS2366 occurs when TypeScript detects that at least one code path can reach the end of the function without encountering a return statement. This commonly happens with: 1. **Incomplete conditional branches**: If/else statements where not all branches return a value 2. **Missing default cases**: Switch statements that don't handle all possibilities 3. **Early returns without fallback**: Functions that return early in some conditions but lack a final return 4. **Async functions with incomplete error handling**: Try/catch blocks where the catch block doesn't return This error is TypeScript's way of preventing runtime bugs where your function might unexpectedly return `undefined` when callers expect a specific type. The strictness helps catch logic errors during development rather than at runtime.
First, analyze your function to find which execution path doesn't return a value. Look for conditional branches that might not return:
// PROBLEM: If condition is false, function returns nothing
function getDiscount(isPremium: boolean): number {
if (isPremium) {
return 20;
}
// Missing return here - function ends without returning a value
}Check your function logic:
- Trace each if/else if/else branch
- Verify every switch case has a return or a default case exists
- Look for early returns that bypass a final return statement
- Check both try and catch blocks in error handling
The most straightforward fix is ensuring every possible path through your function returns a value:
// BEFORE: Error TS2366
function getDiscount(isPremium: boolean): number {
if (isPremium) {
return 20;
}
// Missing return
}
// AFTER: All paths return a value
function getDiscount(isPremium: boolean): number {
if (isPremium) {
return 20;
}
return 0; // Default return for non-premium users
}For switch statements:
// BEFORE: Error TS2366
function getStatus(code: number): string {
switch (code) {
case 200:
return "Success";
case 404:
return "Not Found";
// Missing default case
}
}
// AFTER: Default case added
function getStatus(code: number): string {
switch (code) {
case 200:
return "Success";
case 404:
return "Not Found";
default:
return "Unknown Status";
}
}If your function legitimately may not return a value in some cases, update the return type to allow undefined:
// BEFORE: Error TS2366
function findUser(id: number): User {
if (users.has(id)) {
return users.get(id);
}
// No user found - no return
}
// AFTER: Return type allows undefined
function findUser(id: number): User | undefined {
if (users.has(id)) {
return users.get(id);
}
// Implicit return of undefined is now valid
}
// Or be explicit
function findUser(id: number): User | undefined {
if (users.has(id)) {
return users.get(id);
}
return undefined; // Explicit undefined return
}For async functions:
// BEFORE: Error TS2366
async function fetchData(): Promise<string> {
try {
const response = await fetch(url);
return response.text();
} catch (err) {
console.error(err);
// No return in catch block
}
}
// AFTER: Include undefined in return type
async function fetchData(): Promise<string | undefined> {
try {
const response = await fetch(url);
return response.text();
} catch (err) {
console.error(err);
return undefined;
}
}For discriminated unions, use exhaustiveness checking to ensure all cases are handled:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number }
| { kind: "rectangle"; width: number; height: number };
// BEFORE: Error TS2366 if a new shape is added
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
// Missing rectangle case
}
}
// AFTER: Exhaustiveness check catches missing cases
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
case "rectangle":
return shape.width * shape.height;
default:
// This will error if a case is missing
const _exhaustive: never = shape;
throw new Error(`Unhandled shape: ${_exhaustive}`);
}
}This pattern ensures TypeScript will error if you add a new shape type but forget to handle it in the switch.
If your function has multiple early returns, add a final return statement as a safety net:
// BEFORE: Error TS2366
function processValue(value: string | null): string {
if (value === null) {
return "No value";
}
if (value.length === 0) {
return "Empty";
}
if (value.length > 100) {
return "Too long";
}
// TypeScript can't verify all cases are covered
}
// AFTER: Final return as fallback
function processValue(value: string | null): string {
if (value === null) {
return "No value";
}
if (value.length === 0) {
return "Empty";
}
if (value.length > 100) {
return "Too long";
}
return value; // Fallback for all other cases
}For complex logic, consider refactoring into smaller functions:
function processValue(value: string | null): string {
if (value === null) return "No value";
return validateLength(value);
}
function validateLength(value: string): string {
if (value.length === 0) return "Empty";
if (value.length > 100) return "Too long";
return value;
}To catch these issues earlier in development, enable the noImplicitReturns compiler option in your tsconfig.json:
{
"compilerOptions": {
"noImplicitReturns": true,
"strict": true
}
}This option makes TypeScript check every code path in functions with return types, catching potential bugs where return statements are missing. It works alongside strictNullChecks to enforce consistent return behavior.
You can also enable individual strict options:
{
"compilerOptions": {
"strictNullChecks": true,
"noImplicitReturns": true,
"strictFunctionTypes": true
}
}These settings help prevent common runtime errors by enforcing stricter compile-time checks.
### Understanding void vs undefined
TypeScript distinguishes between void and undefined:
- void: Indicates a function is not meant to return a value (side effects only)
- undefined: A specific value that can be returned and used
// void - no return value expected
function logMessage(msg: string): void {
console.log(msg);
// No return statement needed
}
// undefined - explicitly returns undefined
function maybeGetValue(): string | undefined {
if (Math.random() > 0.5) {
return "value";
}
return undefined; // Explicit return of undefined
}Functions declared with void return type can omit return statements entirely, while functions with other return types (including undefined) must return on all paths.
### Control Flow Analysis Limitations
TypeScript's control flow analysis is sophisticated but not perfect. It may fail to recognize that your code always returns:
function getValue(input: 1 | 2 | 3): string {
if (input === 1) return "one";
if (input === 2) return "two";
if (input === 3) return "three";
// TypeScript might still require a return here
}When this happens, add an explicit final return (even if unreachable):
function getValue(input: 1 | 2 | 3): string {
if (input === 1) return "one";
if (input === 2) return "two";
if (input === 3) return "three";
// Unreachable in practice, but satisfies TypeScript
throw new Error("Unexpected input");
// Or: return "" as never;
}### Asserting Unreachable Code
For unreachable code paths, use a never assertion:
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
function handleValue(input: "a" | "b"): string {
switch (input) {
case "a":
return "handled a";
case "b":
return "handled b";
default:
return assertNever(input); // Ensures exhaustiveness
}
}This pattern leverages TypeScript's type system to guarantee all cases are covered at compile time.
### Common Async Pitfalls
Async functions always return promises. Make sure your return type reflects this:
// WRONG - should return Promise<string>
async function getData(): string {
return fetch(url).then(r => r.text());
}
// CORRECT
async function getData(): Promise<string> {
return fetch(url).then(r => r.text());
}
// With error handling
async function getData(): Promise<string | undefined> {
try {
return await fetch(url).then(r => r.text());
} catch {
return undefined; // Must return in catch block too
}
}### React Component Return Types
React functional components should return JSX or null, not undefined:
// WRONG - components cannot return undefined
function MyComponent(): JSX.Element {
if (!data) {
return; // Error: lacks return statement
}
return <div>{data}</div>;
}
// CORRECT - return null for empty renders
function MyComponent(): JSX.Element | null {
if (!data) {
return null;
}
return <div>{data}</div>;
}Function expression requires a return type
Function expression requires a return type
Value of type 'string | undefined' is not iterable
How to fix "Value is not iterable" in TypeScript
Type 'undefined' is not assignable to type 'string'
How to fix "Type undefined is not assignable to type string" in TypeScript
Type narrowing from typeof check produces 'never'
How to fix "Type narrowing produces never" in TypeScript
Type parameter 'T' has conflicting constraints
How to fix "Type parameter has conflicting constraints" in TypeScript