Skip to content

Instantly share code, notes, and snippets.

@dchowitz
Last active August 30, 2023 06:23
Show Gist options
  • Star 67 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save dchowitz/83bdd807b5fa016775f98065b381ca4e to your computer and use it in GitHub Desktop.
Save dchowitz/83bdd807b5fa016775f98065b381ca4e to your computer and use it in GitHub Desktop.
Debugging ES6 in VS Code

Debugging ES6 in VS Code

My current editor of choice for all things related to Javascript and Node is VS Code, which I highly recommend. The other day I needed to hunt down a bug in one of my tests written in ES6, which at time of writing is not fully supported in Node. Shortly after, I found myself down the rabbit hole of debugging in VS Code and realized this isn't as straightforward as I thought initially. This short post summarizes the steps I took to make debugging ES6 in VS Code frictionless.

What doesn't work

My first approach was a launch configuration in launch.json mimicking tape -r babel-register ./path/to/testfile.js with babel configured to create inline sourcemaps in my package.json. The debugging started but breakpoints and stepping through the code in VS Code were a complete mess. Apparently, ad-hoc transpilation via babel-require-hook and inline sourcemaps do not work in VS Code. The same result for attaching (instead of launch) to babel-node --debug-brk ./path/to/testfile.js with same babel sourcemap configuration as before.

What works

Stepping back from my naive approach, I figured out that transpiling the code beforehand with sourcemaps (set to inline and both both work in package.json, true doesn't), executing the transpiled entry point and then attaching the VS Code debugger worked:

node_modules/.bin/babel src --out-dir .compiled
node --debug-brk .compiled/path/to/testfile.js

The VS Code attach configuration in launch.json:

    {
      "name": "Attach",
      "type": "node",
      "request": "attach",
      "port": 5858,
      "address": "localhost",
      "restart": false,
      "sourceMaps": true,
      "outDir": "${workspaceRoot}/.compiled",
      "localRoot": "${workspaceRoot}",
      "remoteRoot": null
    }

While this worked, I wasn't satisfied since switching back and forth between VS Code and the command line didn't feel right.

Can we do better and automate the transpilation step?

Yes, we can. For that we have to compile the sources immediately before the debug session starts. The launch configuration provides preLaunchTask as pre-debug-hook for this purpose. First, I've added a script in package.json to compile the sources:

{
    ...
    "scripts": {
        ...
        "compile": "rm -rf .compiled && babel src --out-dir .compiled/src"
    }
    ...
}

Unfortunately, we cannot call this script directly in our launch.json since preLaunchTask references tasks configured in tasks.json under .vscode. So let's create a tasks.json file containing our compile task:

{
  "version": "0.1.0",
  "command": "npm",
  "isShellCommand": true,
  "args": ["run"],
  "showOutput": "silent",
  "tasks": [
    {
      "taskName": "compile",
      "isBuildCommand": false,
      "isTestCommand": false,
      "showOutput": "silent",
      "args": []
    }
  ]
}

This just maps our npm script to a task. We could add other npm scripts here as well. Assumed we have a npm script which runs our test suite, we could configure a corresponding task and run our tests by pressing cmd+shift+T, if we set isTestCommand to true.

Finally, we have to configure the launch section in launch.json accordingly:

    {
      "name": "debug",
      "type": "node",
      "request": "launch",
      "program": "${workspaceRoot}/.compiled/path/to/file.js",
      "stopOnEntry": false,
      "args": [],
      "cwd": "${workspaceRoot}",
      "preLaunchTask": "compile",
      "runtimeExecutable": null,
      "runtimeArgs": [
        "--nolazy"
      ],
      "env": {
        "NODE_ENV": "development"
      },
      "externalConsole": false,
      "sourceMaps": true,
      "outDir": "${workspaceRoot}/.compiled"
    }

Now we can press F5 in VS Code and the file configured under program will be debugged.

Can we do even better?

It still feels cumbersome to edit launch.json each time we want to debug a different file. What about debugging the current opened file in VS Code?

To achieve this we somehow have to get the active file into the program property in launch.json. Since VS Code supports variable substitution in tasks.config and launch.config this is not too hard. So is ${file} replaced with the current opened file -- exactly what we need here. The only problem is, that we have to execute the opened file's compiled counterpart.

One solution to overcome this obstacle is to introduce a wrapper script which gets executed for the debug session instead of the file to debug. This wrapper takes the current opened file as single argument, derives its compiled counterpart location and finally just requires the compiled file. The corresponding launch.json section is shown here:

    {
      "name": "debug current file",
      "type": "node",
      "request": "launch",
      "program": "${workspaceRoot}/runcompiled.js",
      "stopOnEntry": false,
      "args": ["${file}"],
      "cwd": "${workspaceRoot}",
      "preLaunchTask": "compile",
      "runtimeExecutable": null,
      "runtimeArgs": [
        "--nolazy"
      ],
      "env": {
        "NODE_ENV": "development",
        "NODE_PATH": "${workspaceRoot}/.compiled/src"
      }

Note the changed properties program and args. The remaining task is to implement runcompiled.js. A simple solution taking advantage of the fact that our src folder is completely mirrored in .compiled is shown below:

// runcompiled.js
// Takes an uncompiled .js file as argument and executes its pendant in .compiled folder.
// Assumes that source files in cwd are mirrored in folder .compiled.

var path = require('path');
var uncompiledFile = process.argv[2];
var compiledDir = path.resolve(process.cwd(), '.compiled');

if (!uncompiledFile) {
  process.stderr.write('filename missing');
  process.exit(1);
}

uncompiledFile = path.resolve(uncompiledFile);

if (uncompiledFile.indexOf(compiledDir) === 0) {
  process.stderr.write(`file in ${compiledDir} not allowed`);
  process.exit(1);
}

var relativePath = path.relative(process.cwd(), uncompiledFile);
var compiledFile = path.join(compiledDir, relativePath);

require(compiledFile);

With those ingredients in place debugging the current opened ES6 file in VS Code is only an F5 away. Enjoy!

Conclusion

I have shown some approaches to debugging ES6 in VS Code from attaching to a node process running the transpiled sources, over automatically transpilation in a pre-debug-hook, and, finally, improving this further to be able to debug the current opened ES6 file with just a keystroke.

Anyway, I still feel that I may have missed something. If that's the case I'm eager hearing about simpler solutions!

@markacola
Copy link

markacola commented Dec 21, 2016

I just set the runtimeExecutable to babel-node in my default Launch configuration and it seemed to work fine with break points in the editor, like so:

{
  "name": "Launch",
  "type": "node2",
  "request": "launch",
  "program": "${workspaceRoot}/src/index.js",
  "stopOnEntry": false,
  "args": [],
  "cwd": "${workspaceRoot}",
  "preLaunchTask": null,
  "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/babel-node",
  "runtimeArgs": [
    "--nolazy"
  ],
  "env": {
    "NODE_ENV": "development"
  },
  "console": "internalConsole",
  "sourceMaps": false,
  "outFiles": []
}

@cellvia
Copy link

cellvia commented Jan 28, 2017

amazing work!!! what a PITA this has been for me. while extremely impressed, I'm going back to sublime until this inordinate number of steps to debug ES6 is ironed out :-/ i have some very simple npm scripts that launch my transpiled code in chrome devtools so i'll just use that. the above feels very fragile, as clever and impressive as it is.

@DavidBabel
Copy link

@markacola i don't think the map is respected this way. You have to enable "sourceMaps": true, if you want your debugger act with corrects lines. If you are lucky it can work, but if you have babel things on top of your compiled file, it will not work as expected.

@noam-honig
Copy link

The answer by @markacola works great for me.

@jmjpro
Copy link

jmjpro commented Aug 31, 2017

@markacola it works for me with node 8.4.0 and the latest version of babel-node. thank you.

@Nirelko
Copy link

Nirelko commented Sep 9, 2017

I think i found a solution that babel register still can work with, my debug configuration is:

      {
        "name": "Launch Electron",
        "type": "node",
        "request": "launch",
        "stopOnEntry": false,
        "runtimeExecutable": "electron",
        "runtimeArgs": [
            "-r",
            "babel-register",
            "./app"
        ]
      }

the problem was that when you gave the runtime arg "-r babel-register" visual studio ran it as string, '-r babel-register', but when i split the commands in order it not to add the redundant apostrophes it worked.

@es-repo
Copy link

es-repo commented Sep 15, 2017

Works for me:

{

  "type": "node",
  "request": "launch",
  "name": "Debug current test file",
  "program": "${workspaceRoot}/node_modules/tape/bin/tape",
  "args": [
    "-r", "babel-register", "${file}"
  ],
  "env": {
    "NODE_ENV": "test"
  }
}

@mrchief
Copy link

mrchief commented Oct 24, 2017

This works for me:

nodemon ./index.js --inspect --exec babel-node
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node2",
      "request": "attach",
      "name": "Node.js 6+",
      "address": "localhost",
      "port": 9229,
      "stopOnEntry": false
    }
  ]
}

I do have to start the server separately (maybe there is a way to do it via launch config) but that works out OK most of the time since I don't need to attach the debugger all the time. This way I just start the server and Attach the debugger only needed (and it attaches to the running process).

I'm using Node 8.4.0 and babel config is contained within my webpack config. The debugger stops at ES6 code at the right places. No precompile is needed.

@jkettmann
Copy link

@mrchief Thanks a lot! This is working great!

@vanhumbeecka
Copy link

My solution is a combination of the proposed solutions in this thread.
I was able to combine nodemon and babel-node with this configuration:

{
            "type": "node",
            "request": "launch",
            "name": "nodemon",
            "runtimeExecutable": "nodemon",
            "args": [
                "--exec", "${workspaceRoot}/node_modules/.bin/babel-node"
            ],
            "program": "${workspaceFolder}/src/services/mappingService.js",
            "restart": true,
            "console": "integratedTerminal",
            "internalConsoleOptions": "neverOpen"
        },

@zpeterg
Copy link

zpeterg commented Jan 1, 2018

@markacola
Your solution seems to work for me, except changing "node2" to "node" and turning on sourcemaps.

@Norfeldt
Copy link

Norfeldt commented Feb 1, 2018

I'm gonna throw mine into the pot as well

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug",
      "program": "${workspaceFolder}/<path to file>",
      "runtimeArgs": ["--es_staging"]
    }
  ]
}

@madcow234
Copy link

@markacola
Thank you for this!
If it helps anyone, I made the following changes:

  1. "node2" is no longer supported, this must be changed to "node". VSCode then suggests to add a "protocol" argument as "inspector", which I actually set to "auto".
  2. Initially, I set a breakpoint on a variable and hit F5, but VSCode changed the breakpoint and all my variables were undefined in the debugger. After setting "sourcemaps" to true, all breakpoints are respected and variables are defined. Thank you to @DavidBabel and @zpeterg.

@Izhaki
Copy link

Izhaki commented May 31, 2018

Note that when using babel when debugging, it is important to enable the sourcemaps and retainLines options.

Here it is as part of .babelrc:

{
  "presets": ["env"],
  "plugins": ["babel-plugin-transform-object-rest-spread"],
  "retainLines": true,
  "sourceMaps": true
}

@AkashBabu
Copy link

AkashBabu commented Jul 15, 2018

@markacola Million Thanks 👍
Only Change is that, you must use "sourceMaps": true
Thanks to @Izhaki as well, now VSCode respects the debugging lines.

Suggestion for the people trying to add extra lines only for debugging. Please add the below code in your file to be debugged and add your custom statements inside it. This piece of code will be run only on running the file directly and will not be run if imported.

if (require.main === module) {
      // your custom statements for debugging
}

@Bala-raj
Copy link

Bala-raj commented Sep 19, 2018

This works for me:

nodemon ./index.js --inspect --exec babel-node
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node2",
      "request": "attach",
      "name": "Node.js 6+",
      "address": "localhost",
      "port": 9229,
      "stopOnEntry": false
    }
  ]
}

I do have to start the server separately (maybe there is a way to do it via launch config) but that works out OK most of the time since I don't need to attach the debugger all the time. This way I just start the server and Attach the debugger only needed (and it attaches to the running process).

I'm using Node 8.4.0 and babel config is contained within my webpack config. The debugger stops at ES6 code at the right places. No precompile is needed.

Thank you very much for sharing this. We just need to add Lunch via NPM which will make your job easy.

package.json

 "scripts": {
    "start": "node ./.build/index.js",        
    "build": "babel application -d .build",
    "dev": "nodemon ./application/index.js --inspect --exec babel-node"
  },`

launch.json

       {
            "type": "node",
            "request": "launch",
            "name": "Start Dev",
            "runtimeExecutable": "npm",
            "runtimeArgs": [
                "run-script",
                "dev"
            ],
            "port": 9229
        } 

@IanSavchenko
Copy link

Thank you, @Izhaki! Your tip regarding "retainLines": true in .babelrc made my day!

@dhirajsharma072
Copy link

I just set the runtimeExecutable to babel-node in my default Launch configuration and it seemed to work fine with break points in the editor, like so:

{
  "name": "Launch",
  "type": "node2",
  "request": "launch",
  "program": "${workspaceRoot}/src/index.js",
  "stopOnEntry": false,
  "args": [],
  "cwd": "${workspaceRoot}",
  "preLaunchTask": null,
  "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/babel-node",
  "runtimeArgs": [
    "--nolazy"
  ],
  "env": {
    "NODE_ENV": "development"
  },
  "console": "internalConsole",
  "sourceMaps": false,
  "outFiles": []
}

👍

@tiendq
Copy link

tiendq commented Apr 1, 2019

It's much easier now, here my launch.json and task.json files, build:server is a npm script to run babel-node.

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Start Debug",
      "program": "${workspaceFolder}/dist/index.js",
      "preLaunchTask": "build",
      "stopOnEntry": true
    }
  ]
}
{
  "version": "2.0.0",
  "tasks": [
    {
      "type": "npm",
      "label": "build",
      "script": "build:server",
      "problemMatcher": []
    }
  ]
}

@gavinmcfarland
Copy link

gavinmcfarland commented Aug 29, 2019

If I'm just using babel to compile my code into a dist folder how can I setup VScode to debug it? I'm just using

babel src -d dist --copy-files && NODE_ENV=test node dist/index.js

to compile the files to a dist folder and then using node to run the application

My .babelrc file has the following in it

{
"sourceMaps": true
}

My launch.json file looks like:

{
	"version": "0.2.0",
	"configurations": [{
		"type": "node",
		"request": "launch",
		"name": "Launch Program",
		"program": "${workspaceFolder}/dist/index.js",
		"sourceMaps": true,
	}]
}

Should the program be pointing at the compiled version or the source code?

@kimfucious
Copy link

I'm glad I came across this gist 😃

The following allows me to launch the debugger from vscode with nodemon and babel-node.

I hope this helps someone.

.babelrc

{
  "presets": ["@babel/preset-env"],
  "env": {
    "debug": {
      "sourceMaps": "inline",
      "retainLines": true
    }
  }
}

launch.json

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "babel-nodemon",
      "program": "${file}",
      "restart": true, // <= important!
      "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/nodemon", // <= local path if nodemon not installed globally
      "args": ["--exec", "${workspaceRoot}/node_modules/.bin/babel-node" ],
      "sourceMaps": true,
      "env": {
        "BABEL_ENV": "debug"
      }
    }
  ]
}

@specimen151
Copy link

@kimfucious's solution works for me. First it did not seem to work, but then I saw that you need to have the file open that you want to debug (but this could probably just be changed in the "program" parameter). Thanks.

@alexander9306
Copy link

I just set the runtimeExecutable to babel-node in my default Launch configuration and it seemed to work fine with break points in the editor, like so:

{
  "name": "Launch",
  "type": "node2",
  "request": "launch",
  "program": "${workspaceRoot}/src/index.js",
  "stopOnEntry": false,
  "args": [],
  "cwd": "${workspaceRoot}",
  "preLaunchTask": null,
  "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/babel-node",
  "runtimeArgs": [
    "--nolazy"
  ],
  "env": {
    "NODE_ENV": "development"
  },
  "console": "internalConsole",
  "sourceMaps": false,
  "outFiles": []
}

Thanks to this answer I was able to make work easily, thank you so much

@Doogiemuc
Copy link

Thank you so much for this gist. Lead me in the right direction to Debug Mocha Tests in VS Code with Babel

@micah-akpan
Copy link

Thank you @dchowitz. If you want to debug your ESModules without first transpiling with babel (that is, using on-the-fly), you can use @babelregister: node --inspect-brk --require @babel/register {server_entry_point}

@Osuriel
Copy link

Osuriel commented May 10, 2021

thanks @markacola idk why this works but it does! i wish i understood why but oh well. at least i can finally debug this crap...

@anabellchan
Copy link

anabellchan commented May 27, 2021

The key to debugging in ES6 is to add sourcemaps files separately from the transpiled code. So after setting up babel on your app, make sure to add sourcemaps that Node can use to debug your code.

Mine works with the following:

npm install

  • @babel/cli
  • @babel/preset-env.

babel.config.js

module.exports = {
  presets: [
    ["@babel/preset-env", {
      "targets": {
        "node": "current"
      }
    }]],
};

package.json

  "scripts": {
    "build": "babel src --out-dir dist --source-maps",
   }

launch.json

   {
      "type": "node",
      "request": "launch",
      "name": "Launch Handler",
      "program": "${workspaceFolder}/dist/handler.js",
      "sourceMaps": true,
    }

Now set a breakpoint on the code then run your debugger for "Launch Handler". The breakpoint will be hit.

@dchowitz
Copy link
Author

Thanks, @anabellchan! Amazing to see that after 5 years, this topic is still a thing 😀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment