What's New in R 4.4 — R 4.4.0 (Puppy Cup) brings experimental
R 4.4.0, nicknamed “Puppy Cup,” landed on April 24th, 2024. Here is what’s new in this release: experimental tail-call optimization with the Tailcall() function, a feature that has long been requested by programmers who work with recursive algorithms. Beyond that, there are important changes to how R handles NULL values and complex numbers that may affect existing code.
TL;DR: The headline feature is Tailcall() for experimental tail-call optimization, which lets recursive functions reuse stack frames instead of growing the call stack. R 4.4 also changes NULL propagation in c(), improves complex number operations, formalizes the native pipe |> behavior, and makes R CMD check stricter about undeclared variables. Most code runs without modification, but test recursive functions and NULL-handling code before upgrading production.
This article covers the changes that matter most for practical R programming.
Experimental tail-Call optimization with tailcall()
Recursion is elegant for problems like tree traversal, factorial calculation, and algorithms that naturally divide into smaller subproblems. The catch is that each recursive call adds a new frame to the call stack, and deep recursion can cause stack overflow errors.
R 4.4 introduces an experimental feature that addresses this: Tailcall(). This function tells R it can reuse the current stack frame instead of creating a new one when the recursive call is in tail position—meaning it is the very last thing the function does.
# Before R 4.4, this could overflow with large n
factorial <- function(n) {
if (n <= 1) return(1)
n * factorial(n - 1)
}
# With R 4.4 tail-call optimization
factorial_tail <- function(n, acc = 1) {
if (n <= 1) return(acc)
Tailcall(factorial_tail, n - 1, n * acc)
}
The acc parameter accumulates the result as we go, making the recursive call the final operation. With Tailcall(), R can optimize this to reuse the stack frame instead of growing the stack infinitely. This is particularly useful for algorithms that would otherwise require thousands of recursive calls.
This is marked experimental, so the behavior may change. It is also not a drop-in replacement for all recursive code—you need to structure functions with tail position in mind. Not every recursive function can be easily converted to use tail calls.
Changes to NULL handling
R 4.4 changes how certain operations handle NULL values, and some existing code may behave differently.
The c() function now preserves NULL differently in certain contexts:
# Previously NULL was dropped silently
# Now behavior is more consistent
x <- list(NULL, 1, NULL)
c_vec <- c(NULL = "a", 1) # Named NULL elements are now preserved in some cases
The is.null() check remains the primary way to test for NULL, but be aware that subsetting operations that previously returned NULL for missing elements may now return something different.
If you have code that relies on specific NULL-coercion behavior, test it with R 4.4 before deploying. The changes are subtle but may catch code that made assumptions about how NULL propagates through various operations.
Complex value improvements
Working with complex numbers in R gets some welcome improvements:
# Complex arithmetic is now more consistent
z <- 1+2i
Conj(z) # Conjugate works as expected
Mod(z) # Modulus
Arg(z) # Argument in radians
# New behavior with NA in complex operations
z_na <- NA_complex_
z_na + 1 # Now properly propagates NA_complex_ instead of coercean incorrectly
The main improvement is that operations involving complex numbers and NA values now behave more predictably and consistently with other numeric types. This fixes edge cases that previously could produce surprising results.
What you should do
If you are upgrading to R 4.4:
-
Test recursive code, If you have functions using recursion, verify they still work. The tail-call optimization is optional but may affect stack behavior in unexpected ways for deeply nested calls.
-
Audit NULL handling, Code that relies on specific NULL-coercion quirks may need updates. Pay attention to code that builds vectors dynamically using
c()with potentially NULL values. -
Check complex number operations, Test any code that mixes complex numbers with NA values. The changes are improvements but may alter the output of existing calculations.
-
Update package dependencies, Make sure your critical packages support R 4.4 before upgrading production environments. Most major packages work, but some older packages may have compatibility issues.
The experimental tail-call feature is the most exciting addition. While it is not enabled by default for all recursive functions, having it available in base R opens up new possibilities for elegant solutions to problems that previously required workarounds or external packages.
Key language changes in R 4.4
R 4.4 formalized the native pipe operator (|>) behavior and improved the lambda function syntax (\(x) expr). The |> operator’s performance is now on par with direct function calls for most use cases, removing the last argument for preferring %>% from magrittr on performance grounds.
R 4.4 also improved the handling of S7, the new object-oriented system designed as a successor to S3 and S4. S7 provides formal class definitions with type-checked properties, method dispatch that combines S3-style simplicity with S4-style rigor, and better tooling for package developers. The R7 package (which was renamed to S7 and integrated into base R) is the way forward for new class hierarchies in R packages.
Tooling and compatibility
R 4.4’s R CMD check is stricter about undeclared global variables and package dependencies, reducing a class of CRAN submission failures that previously required multiple rounds of fixes. The lintr and goodpractice packages catch many of the same issues locally before submission.
Bioconductor 3.19 (released alongside R 4.4) updated over 2,000 packages, with particular focus on single-cell analysis workflows (Seurat, SingleCellExperiment, scran). The bioinformatics community’s adoption of tidyverse conventions, SummarizedExperiment columns accessible with as_tibble(), ggplot2 integration, makes R 4.4 a cohesive platform for multi-omics analysis.
Breaking changes to watch
R 4.4 changed the default value of na.action in some model fitting functions, affecting how missing values are handled in regression models. Code that relied on the previous default behavior may produce different results. Always check options()$na.action in existing code when upgrading to ensure missing value handling matches expectations.
Upgrading
R 4.4 is a minor release, upgrading from 4.3 is straightforward. Most packages compiled against 4.3 continue to work without reinstallation on the same platform. The breaking changes are narrow: code that relied on the deprecated behavior of |> pipe with _ placeholder syntax in non-final position will need updating. Test your package and script collection against 4.4 before switching in production environments.
Upgrading R and reinstalling packages
When upgrading across minor versions (4.3 to 4.4), compiled packages need reinstallation because binary packages are version-specific. pak::pak_upgrade() updates all installed packages. For a clean upgrade: note installed packages with installed.packages(), install the new R version, then restore packages with pak::pkg_install(pkg_list). renv-managed projects restore from the lockfile with renv::restore() regardless of R version.
What version updates mean for analysts
Major R version updates change the language and its standard library. The changes that matter most for analysts are those that affect everyday code: new syntax that simplifies common patterns, behavior changes that break existing code, and performance improvements that make existing operations faster. Tracking these changes matters for both writing new code that uses available features and for understanding why old code might behave differently in a new R version.
R 4.4 continued a pattern of incremental improvements to the base language. The changes in any given minor version are rarely dramatic, but understanding them lets you take advantage of what is available and avoid being surprised when behavior differs from what documentation written for an older version describes.
Backward compatibility considerations
R takes backward compatibility seriously. Code that worked in R 3.6 almost always works in R 4.4 without modification. The one significant breaking change across the R 4.x series was the default for stringsAsFactors in data.frame() changing to FALSE in R 4.0. Code written before R 4.0 that relies on character columns becoming factors automatically breaks in R 4.0 and later. This affects older scripts and packages, particularly those that test column types with is.factor.
The renv lockfile records the exact R version alongside package versions. If your analysis requires a specific R behavior that changed between versions, recording and restoring the full environment, R version plus packages — is the only way to guarantee reproducibility. Posit Package Manager and the Rocker Docker images for specific R versions enable this for production deployments.
Keeping up with changes
The NEWS file distributed with every R release is the authoritative source for what changed. It is dense but comprehensive. The R-bloggers aggregator and the R Weekly newsletter summarize important changes in readable form after each release. For package authors, the CRAN incoming checks and the winbuilder service test packages against the current and development R versions, surfacing compatibility issues before they affect users.
Testing your code against R-devel (the development version) before final release is good practice for package maintainers. The rhub service runs checks across many R versions and platforms. For analysts, testing against the new version in a separate renv project before upgrading the primary environment avoids unexpected breakage at an inconvenient time.
See also
- What’s New in R 4.5, covers the penguins dataset, use(), and parallel downloads
c(), the base combine function with updated NULL handling in R 4.4- Building R Packages, package development workflow including R CMD check usage