oxc-project/oxc · warning · OxcDiagnostic
Expected return with your callback function.
Error message
Expected return with your callback function.
What it means
Diagnostic from oxlint rule node/callback-return (style category, ported from ESLint n/callback-return). In Node callback-style code, after you invoke the completion callback the function must stop: if (err) { callback(err); } followed by more statements runs that extra code and can end up calling the callback twice. The rule detects callbacks purely by callee name — defaults are callback, cb, next, configurable as an array that also accepts dotted paths like obj.method — and demands the call be part of a return statement (or immediately followed by a bare return).
Source
Thrown at crates/oxc_linter/src/rules/node/callback_return.rs:20
use serde::Deserialize;
use oxc_ast::{
AstKind,
ast::{CallExpression, Expression, Statement},
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};
use oxc_str::CompactStr;
use crate::{
AstNode,
context::LintContext,
rule::{DefaultRuleConfig, Rule},
};
fn callback_return_diagnostic(span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("Expected return with your callback function.")
.with_help("Return the callback call or add an explicit return immediately after it.")
.with_label(span)
}
#[derive(Debug, Clone, JsonSchema, Deserialize)]
/// The rule takes a single option - an array of possible callback names - which may include object methods. The default callback names are `callback`, `cb`, `next`.
pub struct CallbackReturn(Box<CallbackNames>);
#[derive(Debug, Clone, JsonSchema, Deserialize)]
#[serde(transparent)]
struct CallbackNames(Vec<CompactStr>);
impl Default for CallbackReturn {
fn default() -> Self {
Self(Box::new(CallbackNames(vec!["callback".into(), "cb".into(), "next".into()])))
}
}
View on GitHub (pinned to e1e7af627c)
Solutions
- Prefix the call with return: 'return callback(err);' — the canonical fix
- If the call must not be the return value, add an explicit 'return;' as the very next statement so nothing after it executes
- If the callback genuinely fires exactly once and the warning is a known if/else false positive, rename the parameter so it stops matching callback/cb/next, or disable the rule for the file
- Configure the rule with your real callback names: "node/callback-return": ["error", ["callback", "cb", "next", "obj.method"]]
- Long term, refactor the function to async/await or Promises so the callback disappears entirely
Example fix
// before
function done(err) {
if (err) {
callback(err);
}
callback(); // runs even after the error callback
}
// after
function done(err) {
if (err) {
return callback(err);
}
callback();
} Defensive patterns
Strategy: validation
Validate before calling
// .oxlintrc.json
"rules": {
"node/callback-return": ["error", ["callback", "cb", "next", "done"]]
}
// CI gate
npx oxlint -c .oxlintrc.json --deny-warning . Prevention
- Adopt the habit 'call the callback, then return' — every callback invocation is either return cb(...) or followed by an immediate return
- Review if/else branches flagged by this rule manually: the rule cannot prove single-invocation, so verify each branch calls the callback exactly once
- New code: prefer Promises/async-await so callback flow (and this rule) disappears entirely
When it happens
Trigger: A CallExpression whose callee is a plain identifier or member chain whose source text exactly equals a configured callback name, nested inside a function, where the closest block ancestor is not a ReturnStatement or concise arrow body, and the callback statement is neither the last statement of the function body nor the statement immediately preceding a final 'return'. Canonical trigger: function a(err) { if (err) { callback(err); } horse(); } — the call sits mid-block with code after it.
Common situations: Callback-era Express/MongoDB codebases migrated from ESLint's n/callback-return to oxlint; enabling the whole node preset in a shared config; documented false positive: if/else branches that each call the callback once still warn (static analysis cannot prove single-invocation); documented false negatives: setTimeout(callback, 0), IIFE-wrapped or process.nextTick-nested calls are not matched at all.
Related errors
- Unexpected access to `exports`.
- Unexpected access to `module.exports`.
- Unexpected require().
- Do not mix 'require' and other declarations.
- Do not mix core, module, file and computed requires.
AI-assisted analysis of oxc-project/oxc@e1e7af627c (2026-08-20).
Data as JSON: /api/errors/7bf88a8a15a0bd5a.
Report an issue: GitHub.