VS Code Tasks
VS Code Tasks turn repeated shell commands into one-click operations with problem matchers that pipe errors directly into the Problems tab. Here's how to set them up.
Every project has commands you run over and over: start the dev server, run the compiler, run the linter, run tests. Tasks automate those and bring their output into VS Code — so instead of reading error text in a terminal, you click a file path and jump straight to the problem.
The Anatomy of a Task
Tasks live in .vscode/tasks.json. Open the Command Palette → "Tasks: Configure Task" → "Create tasks.json from template" to get started.
A minimal task looks like this:
{
"version": "2.0.0",
"tasks": [
{
"label": "Start Dev Server",
"type": "shell",
"command": "npm run dev",
"group": "build",
"isBackground": true,
"problemMatcher": []
}
]
}Key fields:
| Field | What it does |
|---|---|
label | The name shown in the task picker |
type | shell for terminal commands (most common) |
command | The actual shell command to run |
group | build or test — affects the default keybinding |
isBackground | true for long-running processes like dev servers |
problemMatcher | How to parse output into Problems tab entries |
Running Tasks
Cmd/Ctrl + Shift + P → "Tasks: Run Task" → pick from the list
Cmd/Ctrl + Shift + B → runs the default build task
Cmd/Ctrl + Shift + T → runs the default test taskOr open the terminal menu → "Run Task."
You can set a task as the default build task by adding "group": { "kind": "build", "isDefault": true }.
Problem Matchers: Turning Output into Clickable Errors
A problem matcher is a pattern that tells VS Code how to read error output. When it matches a line, VS Code creates an entry in the Problems tab — and you can click it to jump to that exact file and line.
You almost never need to write one. VS Code ships with matchers for the most common tools:
{
"problemMatcher": "$tsc" // TypeScript compiler
}
{
"problemMatcher": "$eslint-stylish" // ESLint
}
{
"problemMatcher": "$jshint" // JSHint
}
{
"problemMatcher": ["$tsc", "$eslint-stylish"] // Multiple matchers
}If you're using a popular build tool or linter, there's almost certainly a pre-built matcher for it. Use one of those. Writing a custom matcher (parsing regex for file, line, column, severity, message) is tedious and rarely necessary.
What a custom matcher looks like (just so you know what you're not doing):
{
"problemMatcher": {
"owner": "myTool",
"fileLocation": "relative",
"pattern": {
"regexp": "^(.+):(\\d+):(\\d+):\\s+(warning|error):\\s+(.+)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
}
}Write one if you have a bespoke test runner or build tool with no existing matcher. Otherwise, grab one off the shelf.
Presentation Options
Control how the task uses terminal space:
{
"label": "TypeScript Watch",
"command": "npx tsc --watch",
"isBackground": true,
"presentation": {
"reveal": "silent", // don't focus the terminal when it starts
"panel": "dedicated", // use its own terminal panel
"showReuseMessage": false
},
"problemMatcher": "$tsc-watch"
}reveal options:
"always"— bring the terminal into focus when the task runs (default)"silent"— run in the background, don't steal focus"never"— hide the terminal entirely
panel options:
"shared"— use the same terminal as other tasks"dedicated"— always use its own terminal panel"new"— create a new terminal every run
For long-running background tasks (dev server, TypeScript watcher), use "reveal": "silent" + "panel": "dedicated". They stay running in their own panel, out of your way, but available when you need to see them.
Compound Tasks: Running Multiple Things at Once
Run multiple tasks with a single command:
{
"label": "Start Everything",
"dependsOn": ["Start API Server", "Start Frontend"],
"dependsOrder": "parallel"
}dependsOrder options:
"parallel"— start all at the same time"sequence"— run in order (wait for each to finish before starting the next)
Use "sequence" when order matters — like wiping a database before seeding it:
{
"label": "Reset and Seed DB",
"dependsOn": ["Drop DB", "Run Migrations", "Seed Data"],
"dependsOrder": "sequence"
}Use "parallel" when the tasks are independent — starting a frontend and backend at the same time.
A Practical tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "TypeScript: Watch",
"type": "shell",
"command": "npx tsc --watch",
"isBackground": true,
"problemMatcher": "$tsc-watch",
"presentation": {
"reveal": "silent",
"panel": "dedicated"
},
"group": { "kind": "build", "isDefault": true }
},
{
"label": "Dev Server",
"type": "shell",
"command": "npm run dev",
"isBackground": true,
"problemMatcher": [],
"presentation": {
"reveal": "silent",
"panel": "dedicated"
}
},
{
"label": "Test: Watch",
"type": "shell",
"command": "npm run test:watch",
"isBackground": true,
"problemMatcher": "$jest",
"presentation": {
"reveal": "silent",
"panel": "dedicated"
},
"group": { "kind": "test", "isDefault": true }
},
{
"label": "Start All",
"dependsOn": ["TypeScript: Watch", "Dev Server"],
"dependsOrder": "parallel"
}
]
}The Hidden Benefit: Clickable File Paths
Even without a problem matcher, anything in your terminal that looks like a file path with a line number — src/components/UserCard.tsx:42:8 — is Cmd/Ctrl+clickable. VS Code detects that pattern automatically and makes it a link.
Problem matchers are better (they appear in the Problems tab, you can filter by file, severity, etc.), but the raw clickable path is a quick win when you just need to jump somewhere fast.
Checking the box on setup
If you find yourself running the same 2–3 commands every time you open a project, that's the list of tasks worth setting up. The overhead is 10 minutes once, and then it's a menu pick — or a single keyboard shortcut — for the lifetime of the project.
Keep reading
Enjoyed this? Get more like it.
Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.