rlang: the low-level R API the tidyverse is built on
Low-level API for programming with R
At a glance
- What is it?
- rlang is the R package that supplies the data masking machinery, the error and condition system, and the low-level interfaces for environments, calls, symbols and functions that tidyverse packages rely on. As a user you meet it through the embracing operator, and as a package author you meet nearly all of it.
- Who is it for?
- rlang suits a package author writing functions that need to accept a column name or a bare expression from the user, since tidy eval and the embracing operator are the mechanism for that and nothing else in the R ecosystem does it as directly. It does not suit you if you are only analysing data with existing tidyverse verbs, because you will meet rlang only as the {{ operator and you can get the same result from the packages that build on it.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 17 days ago.
- What is it written in?
- Mainly R, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two frameworks, and the reason users and authors see different packages
rlang is a collection of frameworks and APIs for programming with R, and the README splits that into two comprehensive frameworks plus a set of argument and interface helpers.
The first framework is tidy eval, a programmable data masking framework used in tidyverse packages like dplyr and ggplot2. As a user, you will encounter the embracing operator {{ and name injection with the glue operators. As a package author, you will encounter the machinery underneath.
That split is the single most important thing to understand about rlang, and it explains why the package feels enormous from one side and invisible from the other. A user of dplyr writes summarise(mean = mean({{ column }})) and never names rlang. The author of a dplyr-like package uses tidy eval, the dots, the calling environment and the condition system, and depends on rlang directly.
The second framework is rlang errors, a set of tools to signal and display errors. This includes backtrace capture with global_entrace() and backtrace display with last_error() and last_warnings(). You use abort() to create errors with bullet lists, structured metadata, and error chaining support.
The display of error messages is optimised for bullet lists and chained errors, and optionally integrates with the cli package through local_use_cli().
The lifecycle badge in the README says stable, and the release history backs that up: v1.1.7 on 2026-01-09, v1.2.0 on 2026-04-07 and v1.3.0 on 2026-07-06, with the last push on 2026-10-06. The 1.x line is stable and still moving, and the semantic versioning means a minor bump like 1.2 to 1.3 can add functions without breaking callers.
Data masking, and the {{ operator you already use
The data masking framework is the part users meet, and it is worth being precise about because it is a different model from what most languages offer.
In a normal R function, arguments are promises evaluated in the caller's environment. A function that takes a column name as a string, and one that takes a bare expression, are two different APIs with two different sets of problems. Tidy eval exists to make one function accept both.
The embracing operator {{ is the user-facing half of that. It takes a bare expression and captures it, so that a function can pass it along to dplyr verbs and have the column name resolved correctly at the point of use rather than where the function was defined. The same applies to the glue name injection operators, which let a string be interpreted as a name.
The name injection half is the other piece, and it is the one that shows up in the argument intake section too. When you collect dynamic dots with list2(), the dots support splicing with !!! and name injection with the glue operators. The combination of splicing and injection is what lets a wrapper function forward a variable number of arguments and still let the user name them.
The documentation topic for data masking is the reference for the details, and the argument intake section links to it directly. The practical caution for a package author is that quoting inside a tidy eval function is easy to get subtly wrong, because the difference between a symbol, a string and a captured expression changes how the value is resolved. The dynamic dots documentation is where that distinction is spelled out.
The rlang project ships a skill for working with these idioms, and the repository root contains an AGENTS.md file, which is a sign that the maintainers expect agents to consult the same guidance a human contributor would.
Argument checking, dots and the rest of the intake layer
The argument intake section is the most immediately useful part of the package for anyone writing a function that other people will call.
A set of tools help you check, validate and preprocess arguments. For checking function arguments there are arg_match(), check_required() and check_exclusive(). arg_match() in particular is the one that turns a silently wrong argument into a clear error, by requiring the value to be one of a set of allowed choices and naming the valid ones when it is not. check_exclusive() enforces that at most one of a group of arguments is supplied, and check_required() that a set of arguments are present.
For checking dots there are check_dots_used() and check_dots_empty(). check_dots_empty() is a small function with a large effect: it errors if the caller passed anything you did not expect, which turns a typo in an argument name from a silently ignored value into an immediate complaint. That is the difference between a function that appears to work and one that actually does.
Then there is the collection of dynamic dots with list2(), with the splicing and injection operators described above.
Together these form a coherent layer. You check what the caller gave you, you collect what you cannot enumerate, and you fail early when something is wrong. None of it is exotic, and all of it is the kind of defensive detail that separates an internal helper from a public interface.
The rlang errors framework sits on the same layer from the other side. abort() creates an error with bullet lists, structured metadata and chaining, and the backtrace capture and display functions let you see where an error came from, which is a capability that base R errors do not give you at all.
The low-level interfaces, and why they are the reason to depend on rlang
The programming interfaces section is the part that is really a low-level API, and it is where the value of depending on rlang directly lies.
For the R session there are check_installed(), on_load() and on_package_load(). check_installed() is the polite way to require a package, prompting the user to install it rather than failing outright, and on_load() and on_package_load() handle work that must happen at load time, which is the mechanism for registering things with the package ecosystem.
For environments there are env(), env_has(), env_get(), env_bind(), env_unbind(), env_print() and local_bindings(). Environments are the substrate of R's scoping, and a function that builds one with the right contents and parent is a large part of what metaprogramming in R looks like. local_bindings() is the scoped counterpart, which is how a helper can set variables for the duration of a call without leaving them behind.
For evaluation there are inject() and eval_bare(). inject() splices values into a call before it is evaluated, and eval_bare() evaluates an expression in a given environment without the usual lookup.
For calls and symbols there are call2(), is_call(), is_call_simple(), data_sym() and data_syms(). Building a call as a data structure and then evaluating it is how you write a function that takes code as an argument, and it is also the machinery underneath tidy eval.
For functions there are new_function() and as_function(), the latter supporting the purrr-style formula notation for lambda functions.
Put together, this is a coherent story. rlang gives you the ability to treat code as data, to manipulate environments, to build and evaluate calls, to signal errors with structure, and to check arguments. A package author who needs any two of those has a choice between writing them and depending on rlang, and depending on rlang is the better trade for almost every project.
Installation, licensing and the shape of the repository
Installation from CRAN is the ordinary call:
install.packages("rlang")Or install the development version from GitHub with:
# install.packages("pak")
pak::pkg_install("r-lib/rlang")The pak approach is worth noting because pak handles the dependency resolution and the binary versus source decision, which is a real annoyance when you are installing a development version of a package with a large dependency tree.
The licence is the one thing to check carefully, and it is not a standard permissive licence. The repository is recorded as using a custom licence that GitHub cannot classify, and the top level contains LICENSE, LICENSE.md and LICENSE.note as three separate files. A note file alongside the two licence files usually carries the specific terms, and for a package this central in the R ecosystem, the terms are worth reading before you ship something that depends on it.
The repository layout is that of a mature R package. There is a DESCRIPTION file, a NAMESPACE, an R/ directory for the R code, src/ for compiled code, inst/, man/ for the generated documentation, tests/ for the test suite, a revdep/ directory for reverse dependency checks, a pkgdown/ directory and _pkgdown.yml for the documentation site, NEWS.md, cran-comments.md, CRAN-SUBMISSION and a rlang.Rproj file. There is an air.toml, which is the configuration for the Air R formatter, and a .git-blame-ignore-revs file for excluding formatting-only commits from the blame history. The .claude/ directory and AGENTS.md point at the same agent-facing guidance as the rlang skill.
The codecov badge and the R-CMD-check badge are the two quality signals, and the latter is the CRAN incoming check, which is the same gate any R package has to pass before it is published.
Editorial conclusion
rlang suits a package author writing functions that need to accept a column name or a bare expression from the user, since tidy eval and the embracing operator are the mechanism for that and nothing else in the R ecosystem does it as directly. It does not suit you if you are only analysing data with existing tidyverse verbs, because you will meet rlang only as the {{ operator and you can get the same result from the packages that build on it. Before you adopt it, read the rlang skill and the data masking topic, because the semantics of quoting inside your own functions are easy to get subtly wrong, and note the licence is a custom one recorded in the LICENSE files rather than a standard permissive licence.
Frequently asked questions
What is rlang used for in R?
It provides the tidy eval data masking framework used by packages like dplyr and ggplot2, the rlang errors framework for signalling and displaying errors with backtraces, and low-level interfaces for the R session, environments, evaluation, calls, symbols and functions.
What is the {{ operator in rlang?
It is the embracing operator, the user-facing part of the tidy eval data masking framework. It captures a bare expression so a function can forward it to another package and have the column name resolve at the point of use, which is what lets dplyr-style functions accept a bare column name.
How do I install rlang?
Run install.packages("rlang") for the CRAN release, or install the development version with pak::pkg_install("r-lib/rlang"), which needs the pak package installed first.
How do I create an error with a backtrace in rlang?
Use abort() to signal the error, which supports bullet lists, structured metadata and error chaining. Backtrace capture is done with global_entrace() and backtrace display with last_error() and last_warnings(). The display can optionally integrate with the cli package via local_use_cli().
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/r-lib-rlang)Was this useful?