A plain downloader reports eleven different situations the same way:
something went wrong with a URL. getaca separates them,
because the eleven have three different people who can act on them and
five different things to do.
Every failure is a classed condition carrying an actor
field naming who can act. Callers branch on the class rather than on
message text.
| Condition | Cause | Actor | Action |
|---|---|---|---|
getaca_error_unavailable |
no mirror answered | user | connect, or prefetch elsewhere |
getaca_error_offline |
not cached, network not permitted here | user | prefetch, or point GETACA_CACHE at a seeded cache |
getaca_error_incomplete |
every mirror ended short | user | retry |
getaca_error_credentials |
every mirror refused to serve | user, or author | set the credential named; where none is declared, report it |
getaca_error_upstream_changed |
published bytes were replaced | upstream | the declaring package needs a new record |
getaca_error_cache_corrupt |
local copy drifted from its own record | user | clean and refetch |
getaca_error_redeclared |
the declaration names different bytes for a version already held | author | report to the declaring package |
getaca_error_invalid_registry |
the declaration is wrong | author | report to the declaring package |
getaca_error_declaration |
every mirror agrees, the registry disagrees | author | report to the declaring package |
getaca_error_composition |
the parts arrived intact and compose to something else | author | report to the declaring package |
getaca_error_signature |
a registry that must be signed carried no usable one | author | report to the declaring package |
All eleven inherit from getaca_error, which inherits
from error, so a handler for getaca_error
catches everything the package raises and nothing else.
The general shape
err <- tryCatch(getaca("backbone", registry = reg), getaca_error = function(e) e)
class(err)
#> [1] "getaca_error_offline" "getaca_error_unavailable"
#> [3] "getaca_error" "error"
#> [5] "condition"
err$actor
#> [1] "user"
cat(conditionMessage(err))
#> yourpkg/backbone@2026-06 is not cached and cannot be downloaded (offline mode is in effect).
#>
#> Action: on a connected machine run getaca_prefetch("backbone", package = "yourpkg"),
#> or point GETACA_CACHE at a cache that already holds it.
#>
#> Fix: this is expected during checks. Use getaca_skip_if_unavailable()
#> in tests and getaca_optional() in examples.Three parts to every message: what happened, what to do about it, and
a closing line derived from the actor. The Fix: line is
what tells a user whether the next step is theirs, and the
Action: lines above it are what they type.
Beyond actor, each condition carries the data that
produced it, so a handler can reason about the situation rather than
parse the message:
names(err)
#> [1] "message" "call" "actor" "id"err$id is the getaca_id, so
format(err$id) gives package/name@version for
a log line.
getaca_error_unavailable
No mirror could be reached. Every URL is listed with the reason it failed, so a user can tell a proxy problem from a host that is down from a path that 404s.
getaca("backbone", package = "yourpkg")
#> Error: Cannot reach any source for yourpkg/backbone@2026-06.
#> https://primary.invalid/backbone-2026-06.zip: HTTP 503
#> https://mirror.invalid/backbone-2026-06.zip: Could not resolve host
#>
#> Actions: connect to a network; or run getaca_prefetch() on a connected
#> machine and copy the cache directory; or set the resource optional here
#> with getaca_available() before calling.
#>
#> Fix: see the actions listed above.The condition carries urls and reasons as
parallel vectors, which is enough to build a domain-specific
message:
getaca_error_offline
A subclass of getaca_error_unavailable, raised when the
resource is not cached and the policy in force forbids reaching for it.
Under R CMD check this is the only failure a
resource-dependent code path can produce, because the clamp forces
offline before any transport is attempted.
cat(conditionMessage(err))
#> yourpkg/backbone@2026-06 is not cached and cannot be downloaded (offline mode is in effect).
#>
#> Action: on a connected machine run getaca_prefetch("backbone", package = "yourpkg"),
#> or point GETACA_CACHE at a cache that already holds it.
#>
#> Fix: this is expected during checks. Use getaca_skip_if_unavailable()
#> in tests and getaca_optional() in examples.The subclass relationship is what lets a package write one handler and get both:
inherits(err, "getaca_error_unavailable")
#> [1] TRUESeparating them is worth doing when the two need different words. “You are offline” is wrong advice for a machine that is online but running a check, and “the server is down” is wrong advice for a machine with no network at all.
tryCatch(
getaca("backbone", package = "yourpkg"),
getaca_error_offline = function(e) {
message("Running without network access. Prefetch first:\n",
" getaca_prefetch(\"backbone\", package = \"yourpkg\")")
NULL
},
getaca_error_unavailable = function(e) {
message("The download sources are not responding. Try again later.")
NULL
}
)Handlers are matched in order, so the narrower one has to come first.
getaca_error_incomplete
Every mirror that failed did so by ending short of the declared size. That is a different situation from a host being down, and the action is different: retry, because a partial transfer resumes rather than restarting.
getaca("backbone", package = "yourpkg")
#> Error: Transfer of yourpkg/backbone@2026-06 ended early.
#> expected 797000000 bytes, received 412300000 bytes
#>
#> Action: retry. Any previously cached copy was left untouched.
#>
#> Fix: see the actions listed above.The size check runs before hashing, so a truncated four-gigabyte
transfer fails in milliseconds rather than after a full digest. It only
runs when the record declares a size, which is one reason
to declare one.
Where some mirrors ended short and others could not be reached at
all, the broader getaca_error_unavailable is raised
instead, with each mirror’s reason listed. Retrying helps one kind of
failure and not the other, so claiming the whole thing was a truncation
would be wrong.
getaca_error_credentials
Every mirror that failed answered 401 or 403. The resource is reachable and the machine has a network; what is missing is permission. Telling that user to connect to a network would send them looking in the wrong place, which is the same reasoning that separates a truncation from an outage.
getaca("gedi-l4a", package = "canopy")
#> Error: Access to canopy/gedi-l4a@2026-06 was refused.
#> data.ornldaac.earthdata.nasa.gov: bearer credential from EARTHDATA_TOKEN (not set: EARTHDATA_TOKEN)
#> obtain one at https://urs.earthdata.nasa.gov/users/new
#>
#> Actions: set the variables named above and retry; or, if a credential is
#> already set, check that it is current and carries access to this resource.
#>
#> Fix: see the actions listed above.Which variable is wanted comes from the declaration rather than from the response, because a server that never saw a credential and one that rejected the credential it saw answer alike. The message says which of the two happened, since “not set” and “set, and refused” call for different next steps.
err$demands carries one entry per host, each with
host, scheme, variables,
missing and register. Nothing in the condition
holds a credential; the value is read at the moment of the request and
never stored.
This is the one condition whose actor depends on the
situation. Where the declaration names a credential, the person who can
act is the user holding it. Where a host refuses and the declaration
names none, there is no variable to set: either the resource became
restricted after it was declared or the location is wrong, and both are
the declaring package’s to fix, so actor is
"author" and the message says so instead of asking for a
variable that does not exist.
To ask before fetching rather than after failing,
getaca_credentials() reports every variable a package’s
declaration reads and whether it is set:
getaca_credentials(package = "canopy")
#> package host scheme variable set
#> 1 canopy data.ornldaac.earthdata.nasa.gov bearer EARTHDATA_TOKEN FALSE
#> register
#> 1 https://urs.earthdata.nasa.gov/users/newSee vignette("declaring") for how a package declares
one.
getaca_error_upstream_changed
A complete transfer whose digest does not match the declaration. The publisher appears to have replaced the file without issuing a new version.
getaca("backbone", package = "yourpkg")
#> Error: The remote file no longer matches yourpkg/backbone@2026-06.
#> declared SHA-256: 9f9f9f...
#> observed SHA-256: 3c1a77...
#> source: https://primary.invalid/backbone-2026-06.zip
#>
#> The publisher appears to have replaced the contents without issuing a
#> new version. getaca will not accept the substitution, and any copy
#> already verified in the cache has been left untouched.
#>
#> Fix: the publisher changed the file. The declaring package needs a new
#> registry entry.Two properties of this failure are the point of the package. The new bytes are not accepted, so nothing downstream silently starts computing on a different dataset. And a copy already in the cache is untouched, so a machine that has the correct file keeps working while the situation is sorted out.
As a user, there is nothing to do here except report it. As the declaring author, the response is to work out which of four things happened: a corrupted transfer that will not reproduce, a genuine upstream recut that deserves a new version record, an upstream mistake worth reporting to the publisher, or a wrong checksum in your own registry.
getaca_error_declaration
Several independent mirrors returned identical bytes, and none matched the declared checksum. When every mirror agrees with the others and disagrees with the registry, the registry is the likely error.
getaca("backbone", package = "yourpkg")
#> Error: The declared checksum for yourpkg/backbone@2026-06 looks wrong.
#> 2 independent sources agreed on SHA-256 3c1a77...
#> the registry declares 9f9f9f...
#>
#> When every mirror agrees with the others and disagrees with the
#> registry, the registry is the likely error.
#>
#> Fix: the declaring package needs a correction. Report it to its maintainer.This is the condition that distinguishes an author’s mistake from a publisher’s. One mirror disagreeing is an upstream mutation; two mirrors agreeing with each other and not with you is a typo, a checksum computed before an upload recompressed the file, or a record copied from the wrong row.
It requires more than one mirror to be diagnosable at all, which is a practical argument for declaring two.
getaca_error_composition
Every declared part() arrived and matched its own
checksum, and combining them produced something other than the artefact
the record names.
getaca("backbone", package = "yourpkg")
#> Error: The parts declared for yourpkg/backbone@2026-09 do not produce the declared bytes.
#> 3 parts, each matching its own checksum, combined by 'concat'
#> declared SHA-256: b1b1b1...
#> composed SHA-256: 2c40f9...
#>
#> Every part arrived intact, so this is not a transfer problem. Either the
#> series is not the one this version is made of, or it is put together by
#> something other than what the registry names.
#>
#> Fix: the declaring package needs a correction. Report it to its maintainer.This is getaca_error_declaration one level up. There,
the transport was sound and the checksum was wrong; here every checksum
in the series was right and what they add up to was wrong. A part that
goes missing or arrives changed raises the ordinary transfer conditions
instead, naming which part of the series it was:
#> Error: Cannot reach any source for yourpkg/backbone@2026-09 (part 2 of 3).As an author, reach for this after re-splitting a file, reordering a
series, or changing what a combiner() does without changing
its id. Nothing reaches the cache, so a user hitting it is not left with
a half-composed artefact.
getaca_error_cache_corrupt
The cached copy no longer matches its own entry record. Raised by the cheap size check on ordinary access, and by the scheduled or forced re-hash.
getaca("backbone", package = "yourpkg")
#> Error: The cached copy of yourpkg/backbone@2026-06 is damaged.
#> path: ~/.cache/R/getaca/yourpkg/backbone/2026-06/raw/backbone-2026-06.zip
#> declared SHA-256: 9f9f9f...
#> observed SHA-256: 71b0c2...
#>
#> Action: getaca_clean("backbone", package = "yourpkg"), then retry.
#>
#> Fix: see the actions listed above.Silent repair was considered and rejected. Refetching without saying anything would hide a disk developing bad sectors, a sync client rewriting files, and a colleague who opened something in the cache directory and saved it. The message names the clean-up call, so the repair is one line and the user knows it happened.
tryCatch(
getaca("backbone", package = "yourpkg"),
getaca_error_cache_corrupt = function(e) {
warning("The cached backbone was damaged; refetching.", call. = FALSE)
getaca_clean("backbone", package = "yourpkg")
getaca("backbone", package = "yourpkg")
}
)That handler is reasonable in a package that knows its users would
want the repair. Doing it inside getaca for everyone would
make the failure invisible.
getaca_error_redeclared
The cached copy still matches its own record, and the declaration in force has moved to different bytes for that version. Both are intact, which is what separates this from a corrupt cache:
getaca("backbone", package = "yourpkg")
#> Error: The declaration for yourpkg/backbone@2026-06 no longer names the bytes already held.
#> cached SHA-256: 9f9f9f...
#> declared SHA-256: 71b0c2...
#>
#> A version identifies exact bytes, so these cannot both be that version.
#> The cached copy was verified against the earlier declaration and has
#> been left untouched. Publish a new version instead of redefining this
#> one.
#>
#> Action: if the new declaration is the correct one, drop the old copy
#> with getaca_clean("backbone", package = "yourpkg") and retry.
#>
#> Fix: the declaring package needs a correction. Report it to its maintainer.Resolving quietly to the copy already held would be the wrong answer:
it makes one machine keep bytes every other machine has stopped using,
and the disagreement stays invisible for as long as that cache survives.
verify = TRUE asks the same question, so a forced re-hash
cannot confirm bytes against a declaration they no longer match
either.
A version reaches this state when a package upgrade brings an edited
record for a version this machine has already fetched. A remote registry
or a pin file trying the same thing is stopped earlier, at
getaca_error_invalid_registry, since neither is allowed to
redefine a published version at all.
getaca_error_invalid_registry
The declaration is malformed or internally inconsistent. Raised at
registry() and resource() where possible,
which puts it on the author’s machine rather than a user’s:
registry("yourpkg", list(
resource("backbone", "2026-09",
urls = "https://host.invalid/a",
sha256 = strrep("ab", 32)),
resource("backbone", "2026-03",
urls = "https://host.invalid/b",
sha256 = strrep("cd", 32))
))
#> Error:
#> ! Invalid getaca registry for package 'yourpkg'.
#> - resource 'backbone' declares 2 versions (2026-09, 2026-03) but the registry names no current one; add current = c("backbone" = "2026-03")
#>
#> Fix: the declaring package needs a correction. Report it to its maintainer.Every problem found is listed, rather than the first one:
resource("backbone", "2026-06",
urls = "ftp://host.invalid/a", sha256 = "abc")
#> Error:
#> ! Invalid getaca registry.
#> - resource 'backbone': all URLs must use https
#> - resource 'backbone': `sha256` must be 64 lowercase hex characters
#>
#> Fix: the declaring package needs a correction. Report it to its maintainer.The same condition covers the runtime cases: a package that ships no registry, a name the registry does not declare, a remote registry declaring a different package, and a pin file that redefines a published version.
resolve_resource("no-such-resource", registry = reg)
#> Error:
#> ! Invalid getaca registry for package 'yourpkg'.
#> - package 'yourpkg' declares no resource named 'no-such-resource' (has: backbone)
#>
#> Fix: the declaring package needs a correction. Report it to its maintainer.The message names what is on offer, which is usually enough to spot a typo without opening the registry.
resolve_resource("backbone", package = "stats")
#> Error:
#> ! Invalid getaca registry for package 'stats'.
#> - package 'stats' ships no getaca registry at inst/getaca/registry.rds
#>
#> Fix: the declaring package needs a correction. Report it to its maintainer.That one names the conventional path, so a user who expected a package to declare resources can see immediately that it does not.
getaca_error_signature
A remote registry arriving without a signature this package’s own
keys accept. Raised only where the bundled registry declares
keys; a package declaring none never fetches a signature
and never meets this:
getaca("backbone", package = "yourpkg")
#> Error: The registry for 'yourpkg' could not be established as authentic.
#> no signature could be fetched and read
#> source: https://yourpkg.invalid/getaca-registry.rds.sig
#>
#> This package declares signing keys, so a remote registry that cannot be
#> checked against one is refused rather than used. The bundled declaration
#> the package ships is unaffected and still resolves.
#>
#> Action: getaca_policy("bundled") resolves through it for this session.
#>
#> Fix: the declaring package needs a correction. Report it to its maintainer.The same condition covers a signature from a key the package does not declare, one whose expiry has passed, and one covering a declaration state older than the installed registry, which is what stops an old genuine registry being replayed in place of the current one.
A host that cannot be reached is a different situation and gets the other answer: resolution falls back to the bundled declaration with a message. A registry that arrives and fails verification stops instead. A registry arriving without its signature is the second, since one host serves both, and treating a missing signature as an outage would let whoever serves the file decide whether the check runs at all.
See vignette("policies") for how a package declares keys
and signs what it publishes.
Writing a handler in a declaring package
The pattern that works is to catch the user-actionable conditions and speak your own domain, and to let the author-actionable and upstream-actionable ones through unchanged. A user cannot act on either, and the default messages already say who can.
install_backbone <- function(name = "backbone") {
tryCatch(
open_backbone(getaca(name, package = "yourpkg")),
getaca_error_offline = function(e) {
stop("The ", name, " backbone is not installed and this session cannot ",
"download it.\n",
"On a connected machine run:\n",
" yourpkg::install_backbone(\"", name, "\")\n",
"or point GETACA_CACHE at a directory that already holds it.",
call. = FALSE)
},
getaca_error_unavailable = function(e) {
stop("The ", name, " backbone could not be downloaded from any of its ",
length(e$urls), " sources.\n",
"This is usually temporary. Try again, or use a smaller reference:\n",
" yourpkg::analyse(x, backbone = \"grid\")", call. = FALSE)
},
getaca_error_cache_corrupt = function(e) {
stop("The cached ", name, " backbone is damaged.\n",
"Repair it with:\n",
" getaca::getaca_clean(\"", name, "\", package = \"yourpkg\")",
call. = FALSE)
}
)
}Three things this gets right. It names your function rather than
getaca(), so the user’s next step is in your vocabulary. It
suggests a domain-level alternative for the transient case. And it
leaves getaca_error_upstream_changed and
getaca_error_declaration alone, which means a user who hits
one gets a message that tells them to report it, and a report that
contains the checksums you need.
Testing your handlers
Conditions can be constructed by making a resolution fail, which needs no network:
offline_error <- tryCatch(
getaca("backbone", registry = reg, policy = "offline"),
getaca_error = function(e) e
)
class(offline_error)[1]
#> [1] "getaca_error_offline"
test_that("a missing backbone gets a yourpkg-flavoured message", {
withr::local_envvar(c(GETACA_OFFLINE = "true"))
withr::local_options(list(getaca.cache = withr::local_tempdir()))
expect_error(install_backbone("backbone"), "On a connected machine run")
})Forcing GETACA_OFFLINE with an empty cache is the
cheapest way to exercise the path a user without the resource takes, and
it is deterministic on every machine.
For the conditions that need a transfer, the honest test is against a
real resource on CI with NOT_CRAN=true, guarded by
getaca_skip_if_unavailable(). See
vignette("checks").
Where to go next
vignette("checks")for behaving during checks and in CIvignette("cache")for whatgetaca_error_cache_corruptis protectingvignette("declaring")for the two conditions that name you