Learn why TypeScript throws TS7030 when a function returns a value on some paths but not others under noImplicitReturns, and how to fix every branch.
TS7030 means your function returns a value on one path and simply runs off the end on another. Some branch hands back a string, and a different branch — the else you never wrote, the default you left off the switch, the catch that only logs — reaches the closing brace instead. JavaScript quietly returns undefined there, and callers that expected a real value get one anyway: undefined.
The check lives behind the noImplicitReturns compiler option. It is not part of strict, so tsc --strict alone will not report it — you have to enable it in tsconfig.json or pass --noImplicitReturns. That is why this error tends to show up all at once: someone copies a stricter tsconfig, or CI runs with a different config than your editor, and suddenly a dozen functions light up in code that has worked for years.
The rule is narrower than it first looks. The compiler reports a function only when all of the following hold:
function classifyAmount(amount: number) {
// ~~~~~~~~~~~~~~ Error: Not all code paths return a value. (TS7030)
if (amount > 0) return "credit" // (1) at least one `return <value>`
} // (2) the end of the body is reachable
// (3) the return type is not void / any / exactly undefined
Drop any one of those and the check is skipped. A function annotated : void can return bare all day; a function whose inferred return type is any (a return JSON.parse(raw) in a try, for instance) is ignored; and a function with no value returns at all is a different diagnostic. If the return type is annotated without undefined — say : string — you get TS2366 instead, because then the missing return is an assignability problem, not just a style one.
Every ❌ sample below is compiled with --strict --noImplicitReturns. Without that second flag none of them report anything.
if With No Final ReturnThe most common shape by far. The happy path returns, the unhappy path was never written, and the function's inferred type becomes string | undefined without anyone deciding that.
// ❌ Broken
function classifyAmount(amount: number) {
// ~~~~~~~~~~~~~~ Error: Not all code paths return a value. (TS7030)
if (amount > 0) {
return "credit"
}
}// ✅ Fixed — the last branch returns too, and the type is now plain `string`
function classifyAmount(amount: number): string {
if (amount > 0) {
return "credit"
}
return "debit"
}Annotating the type does not help here. function classifyAmount(amount: number): string | undefined still reports TS7030, because the flag cares whether you wrote the return, not whether undefined is allowed. If falling through really is the intent, say so with return undefined.
map, filter, sort and reduce callbacks are prime territory, because a block-bodied arrow function looks so much like a statement that it is easy to forget it has to produce a value for every element.
// ❌ Broken
interface Order {
total: number
}
declare const orders: Order[]
const sizeLabels = orders.map((order) => {
// ~~~~~~~~~~ Error: Not all code paths return a value. (TS7030)
if (order.total > 100) {
return "large"
}
})sizeLabels would be (string | undefined)[] — an array with holes in it that only shows up as a bug much later, when something renders undefined.
// ✅ Fixed — an expression body has exactly one path
const sizeLabels = orders.map((order) => (order.total > 100 ? "large" : "small"))And if you never wanted the resulting array, the callback was not a map at all:
// ✅ Also fixed — forEach expects no return value
orders.forEach((order) => {
if (order.total > 100) {
console.log(order.total)
}
})switch With No defaultRoute guards, reducers and permission checks all tend to grow one case per known value and stop there. To the compiler, an unlisted value walks straight past the switch and out of the function.
// ❌ Broken
function canActivate(role: string) {
// ~~~~~~~~~~~ Error: Not all code paths return a value. (TS7030)
switch (role) {
case "admin":
return true
case "editor":
return false
}
}// ✅ Fixed — a default clause closes the last path
function canActivate(role: string): boolean {
switch (role) {
case "admin":
return true
case "editor":
return false
default:
return false
}
}The better fix is often to make the input honest. If role can only ever be one of two values, type it as a union — the compiler then proves the switch is exhaustive and asks for nothing extra:
// ✅ Fixed — no default needed, the switch covers the whole union
type Role = "admin" | "editor"
function canActivate(role: Role): boolean {
switch (role) {
case "admin":
return true
case "editor":
return false
}
}catch Block That Only LogsA try returns the parsed value, the catch writes to the console, and nothing returns on the error path. This one is worth fixing carefully, because the implicit undefined is exactly the value your callers will forget to check.
// ❌ Broken
interface UserProfile {
id: string
}
function parseProfile(raw: string): UserProfile | undefined {
// ~~~~~~~~~~~~~~~~~~~~~~~ Error: Not all code paths return a value. (TS7030)
try {
return JSON.parse(raw) as UserProfile
} catch (error) {
console.error("invalid profile payload", error)
}
}// ✅ Fixed — the failure path states its result
function parseProfile(raw: string): UserProfile | undefined {
try {
return JSON.parse(raw) as UserProfile
} catch (error) {
console.error("invalid profile payload", error)
return undefined
}
}Note that removing the as UserProfile cast alone does not silence the error: the : UserProfile | undefined annotation still pins the return type, and noImplicitReturns keys off the annotated type, so TS7030 still fires. Drop the annotation as well and the return type is inferred from JSON.parse as any, which the check skips. That is not a fix, it is the check losing its footing, and you keep the bug plus an any.
Return on every path. Add the missing return where control currently falls off the end: after the last if, in a default: clause, at the bottom of catch. The compiler points at the function name or its return type annotation, not at the branch that is missing — read the body and find the path that reaches the closing brace.
Prefer expression bodies in small callbacks. A conditional expression has one path by construction, so the whole class of problem disappears:
[object Object]If you did not need the returned array, the right call was forEach.
Make your unions exhaustive instead of adding a fallback default. Typing the parameter as 'admin' | 'editor' lets TypeScript verify the switch covers everything, which is stronger than a default: return false that silently swallows a new role you add next year. Pair it with a never helper if you want a loud failure on unhandled values:
function assertNever(value: never): never {
throw new Error(`Unhandled role: ${String(value)}`)
}If the function really produces nothing, annotate : void. Side-effect functions that use bare return as an early exit are perfectly legal and are skipped by the check entirely — the error only appears once some branch returns a value. Decide which one the function is and make the signature say it:
function trackOrder(id: string): void {
if (!id) {
return
}
console.log(id)
}Don't reach for the escape hatches. Turning noImplicitReturns off, widening the return type to any, or dropping a // @ts-expect-error above the signature all remove the message while leaving the accidental undefined in place. Turning the flag off is a project-wide decision to make deliberately, not a way to unblock one function. And if the errors are coming from inside node_modules, the problem is your include/exclude letting tsc type-check a dependency's sources — fix the config rather than the library. skipLibCheck will not help here — it only skips .d.ts files, and declaration files have no function bodies for TS7030 to come from.
TS7030 is reported when noImplicitReturns is enabled and a function has at least one return with a value while the end of its body remains reachable. Concretely: one branch returns something, another one does not, so the function implicitly hands back undefined on that path. The check is skipped when the return type is void, any, or exactly undefined, and when the function contains no value returns at all. Because noImplicitReturns is not included in strict, the same code passes in a project that never enabled it — which is why the error often appears only in CI or right after adopting someone else's tsconfig.
Work out which path reaches the closing brace and give it a return. In practice that means a final return after the last if, a default: clause in a switch, or a return at the end of a catch block. For short callbacks, replace the block body with a conditional expression so there is only ever one path:
[object Object]If the function is meant to produce nothing on some paths, annotate it : void and use bare return statements — the check then leaves it alone. Widening the return type to any or disabling the flag makes the message go away without answering the question the compiler asked.
undefined?Because noImplicitReturns is a control-flow check, not an assignability check. A return type of string | undefined says undefined is an acceptable result; it does not say you meant to fall off the end of the function to produce it. So the compiler still reports the implicit path and waits for you to write it out:
function classifyAmount(amount: number): string | undefined {
if (amount > 0) {
return "credit"
}
return undefined
}The only annotations that suppress the check outright are void, any, and exactly undefined — a union containing undefined is not enough. Being explicit is the point of the flag: return undefined reads as a decision, whereas a missing return reads as an oversight, and the compiler cannot tell them apart unless you write it down.
Browse all TypeScript practice challenges to keep sharpening your type-level skills.
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges