Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

R Script Progress Bar: Can You Add One Without Loops?

Learn how to add a progress bar in a long-running R script without using loops. Find out what works during deployment with rsconnect or inside Shiny.
R script progress bar without loops, with split screen showing terminal before and after visual progress indicator R script progress bar without loops, with split screen showing terminal before and after visual progress indicator
  • 🔁 R scripts that use functional programming can still show progress without typical loops.
  • 🖥️ The progress and cli packages work well for console tracking in code without loops.
  • 🧑‍💻 In Shiny apps, withProgress() and incProgress() give good UI feedback but must run in reactive blocks.
  • 🚀 Deployment with rsconnect may hide or delay console output unless you make it show up.
  • 🧠 Using environments or closures lets you track state changes in functional code.

If you've written an R script that runs for more than a few seconds, you’ve likely wondered: “Is this thing still working?” Seeing progress can help both developers and users. It makes the experience better and helps with fixing problems. This article shows how to add a progress bar. You can use it in the console or in a Shiny app, even if your code does not use typical loops. We will look at modern ways to do this. These ways work whether you run R scripts on their own, in a Shiny app, or put them on cloud platforms like rsconnect or Posit Connect. And they work with functional code like lapply(), purrr::map(), and pipelines.


When Loops Are Not Used: The Problem

R used for or while loops to go through tasks. This made it simple to track progress with tools like txtProgressBar(). But new ways of coding in R prefer functional programming and clearer approaches. Many R scripts now use things like lapply(), sapply(), purrr::map(), or %>% pipelines. This makes them easier to read and lets you build them in parts. These ways hide the exact steps, which makes it hard to see the progress of each step.

Take this example:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

results <- lapply(files, process_file)

Even though it is still looping underneath, the computer and any tracking tool cannot see when one file ends and another begins. Older progress bar tools cannot work unless you manually step in.

This change makes a big problem in R coding: how to watch hidden loops and keep users updated as code runs, especially when scripts become more complex and take longer.


Tools for R Progress Bars

Many R packages help track progress. You pick the right one based on where your code runs:

Package Environment Console-Friendly Shiny-Compatible Loop-Free Capable
progress (Henry, 2023) Terminal scripts, batch jobs ✅ (manual hooks)
pbapply Apply-family replacements
shiny::withProgress Inside Shiny reactive code
cli::cli_progress_bar Styled terminal environments
progressr Parallel execution and futures ✅ (via handler)

Let's look at each one more closely.

progress Package

Simple, terminal-based progress bars. Good for long-running scripts or Rscript runs in the shell. Does not support Shiny.

pbapply

This provides pblapply(), pbapply(), and more. These add a progress bar to existing apply-family functions. It updates itself for each step. And it works well for projects that still use the apply family of functions.

withProgress() in Shiny

This is best for showing real-time progress in web apps. It needs a reactive block or server-side expression. It works closely with the Shiny session.

cli::cli_progress_bar

You can change how this terminal output looks a lot. It works with or without loops. It also works well inside function calls that are built in parts. And it has tools for logging, finding problems, and loops that are grouped.

progressr

A good choice for future-based parallel or multi-core processing. It automatically sets up handlers for Shiny, RStudio, and the terminal. And you can adjust it well for new execution setups.

These choices make it easier to build strong ways to watch your R script's progress, with or without loops.


Using the progress Package Without Typical Loops

The progress package makes a visual progress bar for the console. It does not work by default with hidden steps. But you can make it work by putting pb$tick() inside the functions you use in your code.

Example: With lapply()

library(progress)

urls <- c("data1.csv", "data2.csv", "data3.csv")
pb <- progress_bar$new(total = length(urls))

process_file <- function(path) {
  pb$tick()
  Sys.sleep(1) # simulate processing
  read.csv(path)
}

results <- lapply(urls, process_file)

When you run this, it processes three files. You will see a live bar update after each tick.

Why It Works

Even though lapply() hides the loop, each time process_file() runs, it gives us a way to manually update the progress. This lets you control things very closely. You can also use it with sapply(), with do.call(), with nested functional maps, and more.

“You must handle ticking the bar manually when using abstracted calls.” (Henry, 2023)

If you use functional programming often, you might want to make your progress tracking standard. You can do this by putting tasks into handlers you wrote that know when to show updates.


Prefer purrr::walk() for Side Effects

The purrr package, made with the tidyverse style, has functions like walk(), map(), map2(), and more. walk() is the best of these for operations that have side effects, like logging or updating progress.

Example: With walk()

library(purrr)
library(progress)

pb <- progress_bar$new(total = 5)

walk(1:5, ~{
  pb$tick()
  Sys.sleep(0.5)
})

This simple setup gives a clear alternative to typical for loops without losing progress updates.

“Vectorized operations can reduce the need for clear loops, but make it harder to track progress.” (Wickham & Henry, 2020)


Adding State Changes in Functional Calls

Pure functions, which only depend on their inputs for output, do not easily allow shared state. But to track progress and still keep your code functional, you can safely put state inside closures or environments.

Technique: Closure-Based Progress Wrapping

pb_env <- new.env()
pb_env$pb <- progress_bar$new(total = 10)

progress_wrapper <- function(func) {
  force(func)
  function(x) {
    pb_env$pb$tick()
    func(x)
  }
}

dummy_task <- function(i) {
  Sys.sleep(0.2)
  i^2
}

result <- lapply(1:10, progress_wrapper(dummy_task))

Closures keep track of pb through all steps, without needing a for loop or global variables. This way works well in functional pipelines.


Shiny App Progress: Real-Time UI Feedback

When writing Shiny apps, showing work is happening helps users trust the app and be more patient.

Classic Case: withProgress()

output$summary <- renderText({
  withProgress(message = "Processing items...", value = 0, {
    for (i in 1:5) {
      Sys.sleep(1)
      incProgress(1/5)
    }
    "Done!"
  })
})

Shiny provides withProgress() and its partner incProgress() for this. These must be used within reactive or observer functions, or they will not work right.

“Progress indicators with withProgress() and incProgress() are made to work inside reactive parts and need to connect to the session.” (Chang et al., 2022)

Advanced: Async Progress Handling

If you're using future or async tasks, think about packages like progressr or Shiny modules. These can report progress using reactive values. You can check these values often to show UI feedback for jobs running in the background.


R Deployment Progress: rsconnect Things to Think About

Deployment platforms like ShinyApps.io or Posit Connect put your code in a sandbox. This means some output types act differently.

Here is what to look for:

  • Output buffering: Text logs show up all at once unless you flush them.
  • Silent failing: print() or cat() statements might fail without showing errors. This happens unless they are in a reactive context or you use flush.console() after them.
  • Visual tools inactive: Console progress bars like progress or cli may not show up remotely. They only appear if you capture them with render functions.

Workaround

cat("Start processing...\n")
flush.console()
Sys.sleep(3) # simulate work
cat("Finished.\n")
flush.console()

“Deployment environments such as rsconnect limit direct console feedback, potentially hiding inline progress unless flushed or encapsulated in outputs.” (RStudio, 2024)

In Shiny, put your progress info into renderPrint() or renderText() directly. And use server logs for finding problems.


Other Ways: Console Output vs. UI Widgets

Where you run your code affects how you show progress:

Scenario Best Progress Method
R script in terminal progress or cli::cli_progress_bar
RMarkdown output pbapply, message(), progressr
Shiny Server withProgress()
Hosted without terminal Logging to file + UI progress widgets
Background multi-core jobs progressr with handlers

Sometimes a visual progress bar is not useful — like in silent batch jobs. In such cases, put logs or timestamps into output files. This helps you find problems even when no one is watching the screen.


Example: File Processing Without a Loop

Let's put everything together with a real-world batch file handler:

library(progress)

files <- list.files("data/", full.names = TRUE)

pb <- progress_bar$new(
  total = length(files),
  format = "  Processing [:bar] :percent | :current/:total files"
)

process_file <- function(f) {
  pb$tick()
  Sys.sleep(0.3) # mimic processing
  df <- read.csv(f)
  summary(df)
}

results <- lapply(files, process_file)

This gives clear feedback in the terminal without changing it to use loops. It takes little effort and makes the script very clear.


Checking Progress in Deployed Code

Checking progress bars when you put your code out there can be hard. Use these ways:

  • Use shiny::runApp() on your computer to act like it's running remotely.
  • Write down outputs with writeLines() or message() to check things.
  • Use message() instead of cat() when making RMarkdown files (it looks better).
  • Put tickers in blocks that you can turn on or off for more detail.

Debug Flag Example

verbose <- TRUE

log_progress <- function(text) {
  if (verbose) message(text)
}

This gives close control during testing without making your live setups messy.


Performance Things to Think About

Watching progress in fast-running operations might slow down your script:

  • Update by batch: Update every n records, not one by one.
  • For UI, slow down updates to stop the screen from getting too busy or flickering.
  • Check memory: progress bars in UIs might stay on screen if you do not clear them.

When speed is important, always test your changed code versus the original. This helps you see if the benefits are worth the cost.


Summary and Things to Know

Showing progress makes your script seem active. Whether you write functional code or put it on the web, there is always a way to tell users what is going on.

Keep in mind:

  • progress and purrr::walk() help track progress in the terminal without loops.
  • withProgress() is the one to use for reactive Shiny work.
  • ✅ When you put code out, flush output, or show logs in the UI.
  • ✅ For functional code, use closures or environments to keep state.
  • ✅ Use batching to lower what the screen shows and to keep code from slowing down.

Progress is not just for developers; it's about the user experience. Make your big tasks more user-friendly with simpler, better feedback.


More R Dev Tips

Want more? Look at tools like future, progressr, and targets for new ways to set up pipelines. Also, think about using reticulate to bring Python work into Shiny. And, look at better RMarkdown templates or tools like make to start builds on their own.

Progress is one part of the whole picture. Make better pipelines, faster dashboards, and smoother deployments — one well-placed pb$tick() at a time.


Citations

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading