eslint no-useless-return
labelled safe
Analysis Nemotron 3 Super
Flags unnecessary 'return;' statements at the end of functions where execution would naturally fall through.
Fix: Remove the 'return;' statement entirely when it appears as the last statement in a function body and control flow would reach the function end without it.
8 hazards
- Removing 'return;' in a function that is the target of a label break changes control flow if the label is outside the function.
- In a function with a finally block, removing a return before the finally may cause the finally block to execute when it previously would not have due to the return skipping it.
- If the function is called via eval or indirect call, removing the return could affect the value of 'this' in strict mode if the function relies on implicit return undefined vs explicit return.
- In a function that is a getter, removing an explicit return; may be misinterpreted by tools expecting a value, though runtime behavior remains undefined.
- If the return statement has a trailing comment, the autofix may drop the comment if not handled properly, altering source formatting.
- In async functions, removing a return; is safe because it still returns a resolved promise with undefined, but tools may misinterpret intent.
- In generators, removing a return; is safe as it yields done:true with undefined, but may confuse static analysis expecting explicit termination.
- If the function is part of a comma-separated expression, removing the return; could change the value produced if the function expression's value is used.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fix ok | finally-execution-after-removed-return | In a function with a finally block, removing a return before the finally may cause the finally block to execute when it previously would not have due to the return skipping it. |
| r1-p002 | fix ok | temporal-dead-zone-access-before-init | Temporal dead zone after code is hoisted or reordered |
| r1-p003 | fix ok | getter-return-undefined-vs-implicit | In a function that is a getter, removing an explicit return; may be misinterpreted by tools expecting a value, though runtime behavior remains undefined. |
| r1-p004 | fix ok | proto-assign-spread-side-effects | __proto__ keys, prototype access, Object.assign vs spread |
| r1-p006 | invalid | label-break-outside-function-target | Removing 'return;' in a function that is the target of a label break changes control flow if the label is outside the function. |
| r1-p008 | fix ok | null-undefined-optional-chaining-nullish-falsy | null/undefined distinction, optional chaining short-circuit, nullish vs falsy |
| r2-p001 | fix ok | Label Break Target Outside Function | Removing 'return;' in a function that is the target of a label break changes control flow if the label is outside the function. |
| r2-p002 | fix ok | Name Collision After Move Due to Shadowing | name collisions across scopes after a move (shadowing) |
| r2-p003 | fix ok | Generator Return Semantics Misinterpreted | In generators, removing a return; is safe as it yields done:true with undefined, but may confuse static analysis expecting explicit termination. |
| r2-p004 | fix ok | ASI Hazard with Leading Parenthesis | ASI and semicolon-less code, leading ( or [ on the next line |
| r2-p005 | silent prediction off | Null/Undef Distinction with Optional Chaining | null/undefined distinction, optional chaining short-circuit, nullish vs falsy |
| r2-p006 | fix ok | Sparse Array Element Access | sparse arrays, holes, Array(n) vs [n], array-likes |
| r2-p007 | fix ok | Async Function Return Value Misinterpretation | In async functions, removing a return; is safe because it still returns a resolved promise with undefined, but tools may misinterpret intent. |
| r2-p008 | invalid | Async/Await Ordering with Yield Position | async/await ordering, generators and yield position |
Sources
r1-p001 · finally-execution-after-removed-return
1function f() {
2 let executed = false;
3 try {
4 return;
5 } finally {
6 executed = true;
7 }
8}
9f();
10console.log('executed:', executed);
--- orig.js 2026-10-22 14:22:33.775903038 -0400 +++ fixed.js 2026-10-22 14:22:39.586151763 -0400 @@ -1,7 +1,7 @@ function f() { let executed = false; try { - return; + } finally { executed = true; }
r1-p002 · temporal-dead-zone-access-before-init
1function f() {
2 console.log(typeof x);
3 var x = 1;
4 return;
5}
6f(); // logs 'undefined', but if return removed, still same
7// However, to make trigger meaningful: add shadowing
8function g() {
9 console.log(typeof y);
10 let y = 2;
11 return;
12}
13g(); // TDZ: ReferenceError if accessed before init
14// But return removal doesn't affect TDZ
15// Instead, use closure capture:
16let h;
17function outer() {
18 let z = 3;
19 h = () => {
20 console.log(z);
21 return;
22 };
23}
24outer();
25h(); // prints 3
26// Now mutate: remove return? no effect.
27// Better: make return affect whether inner fn is called
28function makeCounter() {
29 let count = 0;
30 return function inner() {
31 console.log(count++);
32 return; // TRIGGER
33 };
34}
35const c = makeCounter();
36c(); // 0
37c(); // 1
38console.log('done');
--- orig.js 2026-10-22 14:22:33.776903080 -0400 +++ fixed.js 2026-10-22 14:22:39.536149621 -0400 @@ -1,14 +1,14 @@ function f() { console.log(typeof x); var x = 1; - return; + } f(); // logs 'undefined', but if return removed, still same // However, to make trigger meaningful: add shadowing function g() { console.log(typeof y); let y = 2; - return; + } g(); // TDZ: ReferenceError if accessed before init // But return removal doesn't affect TDZ @@ -18,7 +18,7 @@ let z = 3; h = () => { console.log(z); - return; + }; } outer(); @@ -29,7 +29,7 @@ let count = 0; return function inner() { console.log(count++); - return; // TRIGGER + // TRIGGER }; } const c = makeCounter();
undefined
r1-p003 · getter-return-undefined-vs-implicit
1const obj = {
2 get prop() {
3 console.log('getter entered');
4 return; // TRIGGER: explicit undefined
5 }
6};
7console.log(obj.prop); // logs 'getter entered', then undefined
8// If return removed, still undefined
9// To show difference: compare with actual return value
10const obj2 = {
11 get prop() {
12 console.log('getter2 entered');
13 return 42;
14 }
15};
16console.log(obj2.prop); // 42
17// Now test: if someone expects value, but gets undefined
18try {
19 const val = obj.prop;
20 if (val === undefined) console.log('got undefined as expected');
21 else console.log('unexpected:', val);
22} catch (e) {
23 console.log('error:', e);
24}
--- orig.js 2026-10-22 14:22:33.777903123 -0400 +++ fixed.js 2026-10-22 14:22:40.037171088 -0400 @@ -1,7 +1,7 @@ const obj = { get prop() { console.log('getter entered'); - return; // TRIGGER: explicit undefined + // TRIGGER: explicit undefined } }; console.log(obj.prop); // logs 'getter entered', then undefined
getter entered undefined getter2 entered 42 getter entered got undefined as expected
r1-p004 · proto-assign-spread-side-effects
1function f() {
2 const obj = { a: 1 };
3 Object.__proto__.b = 2; // pollute Object.prototype
4 const copy = Object.assign({}, obj);
5 console.log('b in copy:', 'b' in copy); // true due to proto
6 return; // TRIGGER
7}
8f();
9// After fix: return removed, no change
10// But make it depend on whether f ran:
11let ran = false;
12function g() {
13 ran = true;
14 const o = { x: 1 };
15 Object.__proto__.y = 99;
16 const p = { ...o }; // spread ignores proto
17 console.log('y in p:', 'y' in p); // false
18 return; // TRIGGER
19}
20g();
21console.log('ran:', ran);
--- orig.js 2026-10-22 14:22:33.777903123 -0400 +++ fixed.js 2026-10-22 14:22:38.875121299 -0400 @@ -3,7 +3,7 @@ Object.__proto__.b = 2; // pollute Object.prototype const copy = Object.assign({}, obj); console.log('b in copy:', 'b' in copy); // true due to proto - return; // TRIGGER + // TRIGGER } f(); // After fix: return removed, no change @@ -15,7 +15,7 @@ Object.__proto__.y = 99; const p = { ...o }; // spread ignores proto console.log('y in p:', 'y' in p); // false - return; // TRIGGER + // TRIGGER } g(); console.log('ran:', ran);
b in copy: false y in p: false ran: true
r1-p006 · label-break-outside-function-target (generated program does not parse: /home/sun/dev/diaghunt/runs/2026-10-22T18-22-1)
1outer: {
2 function f() {
3 console.log('in f');
4 break outer; // jumps to after outer block
5 return; // TRIGGER: but unreachable after break
6 }
7 f();
8 console.log('after f call');
9}
10console.log('reached end');
11// If return removed: same, because break outer still exits
12// But to make return reachable: move break inside conditional
13let flag = true;
14outer2: {
15 function g() {
16 if (flag) {
17 console.log('in g, breaking');
18 break outer2;
19 }
20 console.log('in g, not breaking');
21 return; // TRIGGER: only reached if !flag
22 }
23 g();
24 console.log('after g');
25}
26flag = false;
27g(); // now g runs full, returns, then logs 'after g'
28console.log('final:');
r1-p008 · null-undefined-optional-chaining-nullish-falsy
1function f() {
2 const obj = { prop: null };
3 console.log('obj.prop?.length:', obj.prop?.length); // null -> undefined
4 console.log('obj.prop == null:', obj.prop == null); // true
5 console.log('obj.prop === undefined:', obj.prop === undefined); // false
6 return; // TRIGGER
7}
8f();
9// After fix: same output
10// To make return affect outcome: make f return a value used elsewhere
11let captured;
12function getValue() {
13 const x = undefined;
14 console.log('x == null:', x == null);
15 console.log('x === undefined:', x === undefined);
16 return; // TRIGGER: returns undefined
17}
18captured = getValue();
19console.log('captured === undefined:', captured === undefined);
20// If return removed, captured still undefined
21// But make it so that without return, fall-through returns something else?
22// In JS, falling through returns undefined anyway.
23// Instead: use arrow function in map
24const arr = [1, null, 3];
25const result = arr.map(v => {
26 if (v == null) return; // TRIGGER: returns undefined for null
27 return v * 2;
28});
29console.log('result:', result); // [2, undefined, 6]
30// If return removed, still undefined
31// So make it depend on whether function is called:
32let called = false;
33function maybeReturn() {
34 called = true;
35 if (false) return; // never taken
36 // but we want return to be hit
37 return; // TRIGGER: always hit
38}
39maybeReturn();
40console.log('called:', called);
41// Still no change.
42// Final idea: make the function's return value be used in a way that distinguishes
43// explicit return; from fall-through — but they are identical.
44// So instead, focus on the trace depending on whether the function body runs at all.
45// Use a wrapper that only calls if condition.
46let log = '';
47function h() {
48 log += 'h-entered ';
49 return; // TRIGGER
50}
51if (true) h();
52console.log('log:', log.trim());
53// If return removed, same.
54// Give up and make a working version with observable diff via side effect in finally
55// Hazard 8: use nullish coalescing
56function process(val) {
57 if (val == null) return; // TRIGGER: early return for null/undefined
58 console.log('processing:', val);
59 return val * 2;
60}
61const input = null;
62const output = process(input);
63console.log('output:', output); // undefined
64// If return removed, process(null) would log 'processing: null' then return undefined
65// So: with return: no log
66// without return: logs 'processing: null'
67// Perfect.
68// Rewriting hazard 8:
--- orig.js 2026-10-22 14:22:33.778903166 -0400 +++ fixed.js 2026-10-22 14:22:37.690070544 -0400 @@ -3,7 +3,7 @@ console.log('obj.prop?.length:', obj.prop?.length); // null -> undefined console.log('obj.prop == null:', obj.prop == null); // true console.log('obj.prop === undefined:', obj.prop === undefined); // false - return; // TRIGGER + // TRIGGER } f(); // After fix: same output @@ -13,7 +13,7 @@ const x = undefined; console.log('x == null:', x == null); console.log('x === undefined:', x === undefined); - return; // TRIGGER: returns undefined + // TRIGGER: returns undefined } captured = getValue(); console.log('captured === undefined:', captured === undefined); @@ -34,7 +34,7 @@ called = true; if (false) return; // never taken // but we want return to be hit - return; // TRIGGER: always hit + // TRIGGER: always hit } maybeReturn(); console.log('called:', called); @@ -46,7 +46,7 @@ let log = ''; function h() { log += 'h-entered '; - return; // TRIGGER + // TRIGGER } if (true) h(); console.log('log:', log.trim());
obj.prop?.length: undefined obj.prop == null: true obj.prop === undefined: false x == null: true x === undefined: true captured === undefined: true result: [ 2, undefined, 6 ] called: true log: h-entered output: undefined
r2-p001 · Label Break Target Outside Function
1function outer() {
2 inner:
3 {
4 function innerFunc() { return; }
5 break inner;
6 }
7 console.log('A');
8}
9outer();
--- orig.js 2026-10-22 14:22:48.316526362 -0400 +++ fixed.js 2026-10-22 14:22:52.191692963 -0400 @@ -1,7 +1,7 @@ function outer() { inner: { - function innerFunc() { return; } + function innerFunc() { } break inner; } console.log('A');
A
r2-p002 · Name Collision After Move Due to Shadowing
1let x = 1;
2function f() {
3 let x = 2;
4 return;
5}
6f();
7console.log(x); // Should print 1
8// If 'return;' removed and f inlined incorrectly, shadowing breaks
--- orig.js 2026-10-22 14:22:48.317526405 -0400 +++ fixed.js 2026-10-22 14:22:52.116689736 -0400 @@ -1,7 +1,7 @@ let x = 1; function f() { let x = 2; - return; + } f(); console.log(x); // Should print 1
1
r2-p003 · Generator Return Semantics Misinterpreted
1function* g() {
2 yield 1;
3 return;
4}
5const it = g();
6console.log(it.next().value); // 1
7console.log(it.next().done, it.next().value); // true undefined
--- orig.js 2026-10-22 14:22:48.318526448 -0400 +++ fixed.js 2026-10-22 14:22:51.494662982 -0400 @@ -1,6 +1,6 @@ function* g() { yield 1; - return; + } const it = g(); console.log(it.next().value); // 1
1 true undefined
r2-p004 · ASI Hazard with Leading Parenthesis
1function f() {
2 console.log('start')
3 return
4}
5(function() { console.log('IIFE') })()
6// Without fix: ASI inserts ; after return → IIFE runs
7// With bad fix: if return removed, no ASI break → syntax error
--- orig.js 2026-10-22 14:22:48.319526491 -0400 +++ fixed.js 2026-10-22 14:22:52.179692446 -0400 @@ -1,6 +1,6 @@ function f() { console.log('start') - return + } (function() { console.log('IIFE') })() // Without fix: ASI inserts ; after return → IIFE runs
IIFE
r2-p005 · Null/Undef Distinction with Optional Chaining
1let obj = { prop: { value: 42 } };
2function getValue() {
3 return obj.prop?.value;
4}
5console.log(getValue()); // 42
6obj = null;
7console.log(getValue()); // null
8// If getValue had explicit 'return;' at end, removing it doesn't change null
9// But if return; was before null check, hazard appears — here we test baseline
42
r2-p006 · Sparse Array Element Access
1const arr = Array(3);
2arr[1] = 'hello';
3function logElem(i) {
4 console.log(arr[i]);
5 return;
6}
7logElem(0); // undefined
8logElem(1); // hello
9logElem(2); // undefined
10// Removing return; doesn't change output, but shows function called 3 times
--- orig.js 2026-10-22 14:22:48.321526577 -0400 +++ fixed.js 2026-10-22 14:22:51.006641993 -0400 @@ -2,7 +2,7 @@ arr[1] = 'hello'; function logElem(i) { console.log(arr[i]); - return; + } logElem(0); // undefined logElem(1); // hello
undefined hello undefined
r2-p007 · Async Function Return Value Misinterpretation
1async function f() {
2 console.log('async start');
3 return;
4}
5f().then(v => console.log('resolved with:', v)); // undefined
6// If return; removed, still resolves to undefined
7// But if someone expects early exit semantics, confusion
--- orig.js 2026-10-22 14:22:50.815633781 -0400 +++ fixed.js 2026-10-22 14:22:53.457747436 -0400 @@ -1,6 +1,6 @@ async function f() { console.log('async start'); - return; + } f().then(v => console.log('resolved with:', v)); // undefined // If return; removed, still resolves to undefined
async start resolved with: undefined
r2-p008 · Async/Await Ordering with Yield Position (generated program does not parse: /home/sun/dev/diaghunt/runs/2026-10-22T18-22-1)
1async function* ag() {
2 yield await Promise.resolve(1);
3 return;
4}
5(async () => {
6 const it = ag();
7 const r1 = await it.next();
8 console.log(r1.value, r1.done); // 1 false
9 const r2 = await it.next();
10 console.log(r2.value, r2.done); // undefined true
11})();
12// No return; at end to remove — testing near-miss: yield after await
13}