TL;DR —
Shellstarts a program and returns immediately, before that program has done anything. It hands back a task ID (aDouble), not an exit code, output, or success flag, and the next line of your macro runs while the program is still launching. If you need to wait for it or read what it produced,Shellis the wrong tool — useWScript.Shell(.Runwith wait, or.Exec).
Sub ShellDemo()
Dim taskId As Double
taskId = Shell("notepad.exe", vbNormalFocus) ' returns a task ID, then keeps going
Debug.Print taskId ' e.g. 4180 — NOT an exit code
' the line below runs NOW, while Notepad is still opening
End Sub
Shell is the one VBA verb that reaches outside Excel to start another Windows program. The trouble is
that it behaves nothing like the rest of your macro. Everywhere else, a line finishes its work before the
next one runs, and a failure raises a loud error. Shell breaks both habits: it launches the program and
returns at once, and it tells you almost nothing about whether the program worked. Once you picture it as
fire-and-forget, the puzzling bugs — "my code opened the file before the converter finished writing
it" — stop being puzzling.
What you'll learn
- The mental model —
Shellfires and forgets: it launches, then returns immediately - Why the return value is a task ID, not an exit code or the program's output
- Why the line after
Shellruns too early, and how to actually wait for the program - How to quote a path that contains spaces, and why
Shellalone can't open a PDF or a URL - When to abandon
ShellforWScript.Shell.Run(wait + exit code) or.Exec(capture output) - The errors
Shellraises when the program is missing, and how to guard them
The mental model: Shell fires and forgets
The single idea that explains every Shell surprise: Shell does not run a program — it starts one
and walks away. It asks Windows to launch the executable, gets back a number identifying the new task,
and returns to your macro right away. The program is now running in parallel with your code, not inside
it.
Shell "calc.exe" ' Windows starts Calculator...
MsgBox "done" ' ...and this MsgBox appears instantly, calc still loading
Compare that to a normal method call like Range("A1").Copy, which is finished the instant the next line
runs. Shell is the opposite: it is asynchronous. Your macro and the launched program go their separate
ways. Every rule below is a consequence of this one fact.
The second argument is the window style — vbNormalFocus, vbMinimizedNoFocus, vbHide, and so on. It
controls how the new window appears; it does not make Shell wait. There is no window style that
turns Shell synchronous.
Shell returns a task ID, not a result
Shell returns a Double — the process (task) ID Windows assigned to the new program. People reach
for that number expecting an exit code or a "did it work" flag, and it is neither:
Dim result As Double
result = Shell("robocopy.exe C:\a C:\b", vbHide)
' result is a task ID like 7820 — it says nothing about whether robocopy succeeded
Because the call returns before the program has done its job, there is nothing meaningful for it to report
yet. Shell cannot give you the program's exit code, cannot capture its console output, and does not tell
you if the program later crashed. The task ID is only useful as a handle — for example, to pass to a
Windows API such as OpenProcess/WaitForSingleObject if you decide to wait. If your logic depends on
what the program returned, Shell structurally cannot supply it, and no amount of extra code around
Shell will change that.
Why your next line runs too early
This is the headline bug, and it falls straight out of fire-and-forget. You launch a tool that produces a file, then immediately use that file:
Shell "C:\Tools\convert.exe report.docx report.pdf"
Workbooks.Open "C:\Tools\report.pdf" ' FAILS — the PDF doesn't exist yet
convert.exe needs a second or two to run, but Workbooks.Open runs now, while the converter is barely
starting. The file is missing, and you get a "file not found" error that points at the wrong line — the
real cause is three characters up, in the Shell that did not wait.
The fix is not a fixed Application.Wait or Sleep guess — pause too little and it still fails, pause
too much and you waste the user's time, and either way a slow machine breaks it. To genuinely wait until
the program finishes, you need something other than Shell. The clean answer is WScript.Shell.Run
with its wait flag (below); the low-level answer is the WaitForSingleObject API against the task ID. What
you should stop doing is sprinkling Sleep 2000 and hoping.
Paths with spaces, and opening a document instead of an exe
Two practical traps trip up almost everyone.
Spaces in the path. Shell takes a single command-line string, and a space separates the program from
its arguments. So a path like C:\Program Files\... splits in the wrong place:
Shell "C:\Program Files\App\app.exe" ' error 53 — Windows looks for "C:\Program"
Shell """C:\Program Files\App\app.exe""" ' correct — wrap the exe path in quotes
Inside a VBA string, each "" is one literal quote, so """...""" puts real quotes around the path.
Quote the executable whenever its folder might contain a space (and most do).
Opening a document, not a program. Shell launches executables — it does not know that a .pdf
should open in your PDF reader or that a .xlsx should open in Excel. Pointing it at a document fails:
Shell "C:\Reports\Q1.pdf" ' error 53 — not an executable
Shell "cmd /c start """" ""C:\Reports\Q1.pdf""" ' works — let the shell resolve the default app
The start command (via cmd /c) asks Windows to open the file with whatever program is registered for
it — the same thing a double-click does. The empty "" after start is the window title placeholder,
which start requires when the path is quoted. For opening documents and URLs this way is common; the
cleaner alternative is the ShellExecute API or WScript.Shell, covered next.
When you actually need the result: WScript.Shell
The moment you need to wait for the program, read its exit code, or capture its output, step
up from Shell to the WScript.Shell object, created with
CreateObject:
Dim sh As Object
Set sh = CreateObject("WScript.Shell")
' .Run — third argument True means WAIT until the program exits;
' the return value is then the real exit code.
Dim exitCode As Long
exitCode = sh.Run("robocopy.exe C:\a C:\b", 0, True) ' 0 = hidden window
If exitCode >= 8 Then MsgBox "robocopy failed: " & exitCode
' .Exec — run and read the program's stdout text
Dim p As Object, output As String
Set p = sh.Exec("cmd /c dir C:\")
output = p.StdOut.ReadAll
.Run(command, windowStyle, waitOnReturn) is the drop-in upgrade when you want to wait and get an exit
code. .Exec goes further and gives you a live process object whose StdOut you can read — the only way
to pull a console program's text back into Excel. Build the command's paths from
Environ so they work on any machine, and wrap the whole thing in
error handling so a missing program is reported cleanly instead of crashing.
The honest verdict: Shell for fire-and-forget, WScript.Shell for the rest
Shell is a one-trick verb, and the trick is narrow: start a program and forget about it. It is genuine
and useful for exactly that — kick off a viewer, open a folder, launch a long-running tool you do not need
to track. The bugs come from asking it for things it never promised. Four rules cover the surface:
- It fires and forgets →
Shellreturns immediately; the program runs in parallel. There is no window style that makes it wait. - The return value is a task ID, not a result → no exit code, no output, no success flag. If your
logic needs the program's result,
Shellis the wrong tool. - Quote paths with spaces; documents aren't executables → wrap the exe in
""...""; open a document withcmd /c startorShellExecute, notShelldirectly. - Need to wait or read output? Use
WScript.Shell→.Run(..., True)waits and returns the exit code;.ExeccapturesStdOut. Reach for it the instantShell's silence is a problem.
Match the tool to the job and the "it opened the file before the program finished" class of bug disappears — you were using a fire-and-forget launcher to do a wait-and-check job.
How ExcelMaster helps
The Shell bugs that cost real time are the timing ones: a macro that opens a file the launched program
has not written yet, a Sleep 2000 that works on your machine and fails on a slower one, a converter
whose failure is invisible because Shell reported nothing. Each comes from using a fire-and-forget verb
where you actually needed to wait and check.
ExcelMaster writes the launch-and-wait
logic correctly. Describe the job — "run this converter, then open its output," or "call this command-line
tool and tell me if it failed" — and it picks the right mechanism: Shell when fire-and-forget is truly
fine, or WScript.Shell.Run/.Exec when you need to wait for completion, read an exit code, or capture
output. You describe the program you want to run; it writes the code that runs it and knows when it is
done.
Frequently asked questions
How do I make VBA Shell wait for the program to finish?
Shell itself cannot wait — it always returns immediately. To wait, use the WScript.Shell object
instead: CreateObject("WScript.Shell").Run(command, windowStyle, True). The third argument True
(waitOnReturn) makes it block until the program exits, and the return value is then the program's real
exit code. The alternative is a Windows API approach — pass the task ID Shell returns to
WaitForSingleObject — but WScript.Shell.Run is far simpler for everyday use.
What does the VBA Shell function return?
Shell returns a Double — the task (process) ID that Windows assigned to the newly started program. It
is not an exit code, not the program's output, and not a success flag. Because Shell returns before
the program has finished, there is no result yet to report. The task ID is only a handle you could pass to
a Windows API. If you need the program's exit code, use WScript.Shell.Run(..., True) instead.
Why does VBA Shell give error 53 (file not found)?
Two common causes. First, the path contains a space and is not quoted — Shell "C:\Program Files\..."
splits at the space, so Windows looks for C:\Program; wrap the executable path in doubled quotes:
Shell """C:\Program Files\App\app.exe""". Second, you pointed Shell at a document (a .pdf,
.xlsx) rather than an executable — Shell launches programs, not files. To open a document with its
default app, use Shell "cmd /c start """" ""C:\file.pdf""" or the ShellExecute API.
How do I run a batch file or command-line command from VBA?
For a .bat file, Shell can launch it directly: Shell "C:\scripts\build.bat" (quote the path if it
has spaces). For a raw command that is not an executable — a dir, a copy, a pipe — run it through the
command interpreter: Shell "cmd /c copy a.txt b.txt". If you need to wait for it and read its exit code
or output, use CreateObject("WScript.Shell").Run("cmd /c ...", 0, True) or .Exec instead of Shell.
How can I capture the output of a program run from VBA?
Shell cannot capture output at all. Use the WScript.Shell object's .Exec method, which returns a
process object with a readable StdOut stream:
Set p = CreateObject("WScript.Shell").Exec("cmd /c dir C:\") : output = p.StdOut.ReadAll. This is the
standard way to pull a console program's text back into VBA. For programs that only signal success through
an exit code, use .Run(command, 0, True) and check its return value instead.
Tested in
Tested in: Excel 365 (Windows 11), VBA 7.1 — last verified 2026-08-30.
Related guides: VBA CreateObject & GetObject · VBA Environ · VBA Wait & Sleep · VBA On Error · VBA FreeFile & Open
