emacs
elisp
asynchronous
process
programming

Asynchronously wait for process finish in elisp?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In Emacs Lisp, the normal way to react when a process finishes is not to "wait" in the blocking sense. It is to start the process asynchronously and attach a sentinel that runs when the process status changes. That keeps Emacs responsive and lets your follow-up logic run exactly when the process exits.

Start the Process Asynchronously

make-process is the modern flexible API. It launches the external command and returns immediately.

elisp
1(let ((buffer (get-buffer-create "*demo-process*")))
2  (make-process
3   :name "demo-sleep"
4   :buffer buffer
5   :command '("sh" "-c" "sleep 2; echo done")
6   :noquery t))

This does not block Emacs. You can keep editing, switching buffers, or running other Lisp code while the process executes.

Use a Sentinel for Completion

A process sentinel is the standard callback for process state changes. It is triggered when the process exits, is signaled, or changes status.

elisp
1(defun my/process-finished-sentinel (process event)
2  (when (memq (process-status process) '(exit signal))
3    (message "Process %s finished with event: %s"
4             (process-name process)
5             (string-trim event))))
6
7(let ((buffer (get-buffer-create "*demo-process*")))
8  (make-process
9   :name "demo-sleep"
10   :buffer buffer
11   :command '("sh" "-c" "sleep 2; echo done")
12   :sentinel #'my/process-finished-sentinel
13   :noquery t))

This is the usual answer to "how do I wait asynchronously" in Elisp. You do not pause execution. You register what should happen next.

Capture Output with a Filter or the Process Buffer

If you need the process output before or after completion, either read it from the process buffer or attach a filter.

elisp
1(defun my/process-filter (process output)
2  (message "Output from %s: %s"
3           (process-name process)
4           (string-trim output)))
5
6(make-process
7 :name "echo-demo"
8 :buffer "*echo-demo*"
9 :command '("sh" "-c" "printf 'hello\nworld\n'")
10 :filter #'my/process-filter
11 :sentinel #'my/process-finished-sentinel
12 :noquery t)

The filter runs when output arrives. The sentinel runs when the process status changes. These roles are related, but not interchangeable.

If You Truly Need to Wait, Use accept-process-output

Sometimes code structure forces you to stay in one function until the process produces output or exits. In that case, accept-process-output can wait for process activity while still allowing Emacs to handle I/O. This is more cooperative than a busy loop, but it is still a waiting pattern, not a callback-based design.

elisp
1(let* ((buffer (get-buffer-create "*blocking-demo*"))
2       (proc (make-process
3              :name "blocking-demo"
4              :buffer buffer
5              :command '("sh" "-c" "sleep 1; echo ready")
6              :noquery t)))
7  (while (process-live-p proc)
8    (accept-process-output proc 0.1))
9  (with-current-buffer buffer
10    (message "Final output: %s" (string-trim (buffer-string)))))

This can be useful in tightly controlled code, but it is not the best way to keep a design truly asynchronous.

Choose the Right Mental Model

In Elisp, asynchronous process code works best when you think in terms of events:

  • start the process now
  • attach a filter if you care about streaming output
  • attach a sentinel for completion behavior
  • keep follow-up logic inside the callback or in a function called by it

That pattern fits Emacs itself, which is built around event-driven interaction rather than thread-style blocking control flow.

Common Pitfalls

The most common mistake is calling start-process or make-process and then immediately assuming the process is finished. Process startup is asynchronous, so follow-up logic belongs in a sentinel or another explicit coordination mechanism.

Another mistake is putting too much logic directly into the process filter. Filters can be called many times with partial output chunks, so completion logic usually belongs in the sentinel instead.

Developers also sometimes write polling loops without accept-process-output, which wastes CPU and can make the editor feel clumsy. If you must poll, do it cooperatively.

Finally, remember that process exit and successful completion are not the same thing. A sentinel should often inspect the exit status or event text before assuming the external command succeeded.

Summary

  • In Elisp, the standard asynchronous completion mechanism is a process sentinel.
  • 'make-process starts the command without blocking Emacs.'
  • Use filters for streaming output and sentinels for process completion.
  • 'accept-process-output can cooperatively wait when you truly need a local wait loop.'
  • Event-driven callbacks are usually cleaner than trying to force synchronous control flow onto asynchronous processes.

Course illustration
Course illustration

All Rights Reserved.