v3.29.0: 14 Placement Fixes. Every Log on the Right Line π―
Publication Date: 16/09/2026
Author: Anas Chakroun
6-7 minutes
Turbo inserts a log in one keystroke. The whole promise rests on one detail: the line the log lands on. Put it one line too early and it prints a value that no longer exists. Put it inside an object literal and your file stops compiling. Put it after a return and it never runs.
v3.29.0 is an accuracy release. No new features, no campaign. Just 14 real-world patterns where the insertion engine used to put a log in the wrong place, each one fixed and pinned by a test.

How We Hunted Them
Unit tests check one piece of the engine at a time: the part that recognizes what you selected, or the part that computes the line. Several of these bugs only appear when the two are chained, the way they are when you actually press the shortcut. So this release starts with a new end-to-end test table that runs every snippet through the whole insertion pipeline.
Then we fed the engine more than 700 snippets shaped like production code: React components and hooks, Angular and NestJS services, Express handlers, CommonJS modules, Vue, Svelte, and Astro files. Every insertion was checked against one objective rule: insert the log, then re-parse the file. A parse error, a log after a return, a log outside the scope of its variable, or a log printing the value before the assignment all count as a bug. Every confirmed bug became a failing test first, and a fix second.
The Patterns, and the Fixes
1 β Declarations Logged Inside Their Own Statement
Selecting header in a parenthesized multi-line JSX declaration, or the first variable of a multi-line var a = ..., b = ...; list, inserted the log before the closing ); or between the declarators, which broke the file. A const declared inside a callback passed to an outer declaration, like useState(() => {...}) or a useSWR fetcher, got its log after the outer statement, where it is out of scope. Fix: the log is anchored to the end of the innermost declaration that actually binds the selected name.
2 β Logs Inside the Arguments of new, Type-Asserted and Optional Calls
Selecting a variable initialized by a multi-line new IntersectionObserver(...), <JwtPayload>jwt.verify(...) or obj?.method?.(...) inserted the log on the next line, inside the argument list. Fix: every call-like initializer, including new, type assertions, optional chains, yield and import(), is anchored to the end of its full declaration.
3 β Destructured Defaults Logged Before They Exist
Selecting variant put the log above const {, where the variable is not initialized yet, which throws at runtime. Bindings destructured from a multi-line yield call(...) or await import(...) had the same problem. Fix: destructuring patterns are matched at any depth, including defaults, array patterns and rest elements, so the log goes after the declaration.
4 β Method-Call Objects in Conditions, JSX and Throws
Selecting the object of a method call, like links in links.map(...) or user.roles in if (!user.roles.includes('admin')), inserted the log right after the call: inside JSX markup or an argument list, inside the if body, between chained calls, or after a throw where it never runs. Fix: the log is placed relative to the enclosing statement, before a condition, a throw, or a multi-line statement the call is only part of, and after the statement otherwise.
5 β Keys of Returned, Passed and Exported Objects
Selecting id inserted the log inside the object literal, a syntax error, or after the return, where it never runs, and it printed the bare key, which is not a variable. emit({ kind }), fetch(url, { body }) and export default { name } behaved the same. Fix: the log prints the property's value, user.id, before the enclosing statement.
6 β Keys of Objects Assigned to a Property
Selecting ready inserted the log inside the literal and printed only ready. Keys of declared objects wrapped in as const or satisfies also lost their variable name in the logged path. Fix: the log prints the full path, such as this.state.ready or module.exports.a, after the assignment.
7 β Parameters of One-Line Function Bodies
Selecting a parameter of a function whose body sits on its signature line, like this setter, an empty stub onBlur(event) {}, or const onSubmit = (event) => {};, put the log in the class body, before the function, or between object properties. Fix: the body is opened onto its own lines and the log goes at its top, as it already did for empty functions.
8 β Arrows Rewritten Without Owning the Variable
Selecting user rewrote the selector arrow so it logged user inside its own initializer, which throws, only because the arrow reads a property named user. Object keys and nested methods triggered the same wrong rewrite. Fix: an arrow is rewritten only when it owns the selected variable, as a parameter or a real reference in its body.
9 β switch, export default, Labels and throw
Selecting action.type inserted the log above the function, where action does not exist. export default memo(Card), TypeScript import fs = require('fs') and labelled loops were misplaced in similar ways, and a value inside a single-line throw was logged after it. Fix: these are statement boundaries now: heads are logged before the statement, exports and imports after it, and throws before the throw.
10 β Catch Parameters
Selecting err inserted the log above the try, where the error binding does not exist. Fix: catch parameters, plain or destructured, are treated like function parameters and logged at the top of the catch body.
11 β Else-If Conditions
Selecting user.role inserted the log at the end of the !user branch, so it only ran when user was missing, and then it threw. Fix: else-if conditions are logged at the top of the else-if body.
12 β Callbacks Nested in a return
Selecting item.label put the log before the outer return, where item does not exist. Inline handlers, returned closures, effect cleanups and Promise executors had the same problem. Fix: a selection belongs to the function whose body contains it, and it is logged there.
13 β Multi-Line Member Reassignments
Selecting globalThis.prisma logged it before the assignment, printing the old value. module.exports = {...} at the very start of a file did the same. Fix: every reassignment of a member, with any operator, is logged after the full statement.
14 β Multi-Line Variable Reassignments
Selecting connection, or any variable reassigned to a multi-line object, awaited call or ternary without const or let, logged it before the statement, printing the previous value. A plain ||= call could even put the log inside the object argument. Fix: variable reassignments, with any operator, are logged after the full statement, like declarations.
Found the Next One?
A table of 700 snippets is still smaller than the code you write every day. If Turbo places a log in the wrong spot, the fastest way to get it fixed is to show us the snippet.
π¬ Report an Edge Case
Paste the snippet, tell us what happened and what you expected on the Edge Cases Reporting page. Every report becomes a failing test before it becomes a fix.
Why This Is Free, and What Turbo Pro Pays For
Every fix in this release ships to every Turbo user, free, like every improvement to log insertion since 2018. That work is funded by Turbo Pro.

Turbo puts the logs in. Turbo Pro takes them out. It removes the debug logs in your changed lines the moment you commit, always previewed first, with the scope under your control. It also gives you a workspace-wide log tree, instant search, real-time filtering by log type, git-aware filtering, and bulk cleanup by type and scope. It is a one-time payment with lifetime access, and every future update is included.
Not a subscription. One-time purchase, lifetime access, every future update included. Every license funds the next accuracy release.
v3.29.0 is already running in your editor. All 14 fixes are live. Select a variable in the next catch block, else-if branch, or reassignment you write, press the shortcut, and check the line.