astrid-runtime/astrid · error · ContextualIoError

{context}

Error message

{context}

What it means

with_context is the library's generic error-wrapper: it re-packages an underlying io::Error into a new io::Error of the same ErrorKind whose Display message is the supplied context string and whose source() chain preserves the original error. Hitting this means an OS operation failed and the library added human-readable context about which guarded file operation failed. Inspect the error's source() chain for the root cause (e.g. sharing violation, access denied, path not found).

Source

Thrown at crates/astrid-core/src/platform_fs/windows/error.rs:28

struct ContextualIoError {
    context: String,
    source: io::Error,
}

impl fmt::Display for ContextualIoError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}: {}", self.context, self.source)
    }
}

impl std::error::Error for ContextualIoError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        Some(&self.source)
    }
}

pub(super) fn with_context(error: io::Error, context: impl Into<String>) -> io::Error {
    io::Error::new(
        error.kind(),
        ContextualIoError {
            context: context.into(),
            source: error,
        },
    )
}

#[cfg(test)]
pub(super) fn native_error_code(error: &io::Error) -> Option<i32> {
    if let Some(code) = error.raw_os_error() {
        return Some(code);
    }
    let mut source = error.source();
    while let Some(current) = source {
        if let Some(error) = current.downcast_ref::<io::Error>()
            && let Some(code) = error.raw_os_error()
        {

View on GitHub (pinned to affd8760f4)

Solutions

  1. Inspect err.source() (walk the chain) to find the original io::Error kind and OS error code
  2. Retry the operation if the source kind was transient (sharing violation / lock temporarily held)
  3. Close other handles to the file (other processes, editors, AV scans) before retrying
  4. Run with sufficient privileges for operations inside the trusted directory

Example fix

// before: matching only on the wrapped message
if err.to_string().contains("replace") { ... }
// after: unwrap the context to branch on the root cause
let mut src: Option<&(dyn std::error::Error + 'static)> = err.source();
while let Some(e) = src { eprintln!("caused by: {e}"); src = e.source(); }
if err.kind() == io::ErrorKind::PermissionDenied { /* escalate or fix ACL */ }
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight the guarded operation's target: existence and writability
fn can_modify(path: &std::path::Path) -> bool {
    match std::fs::OpenOptions::new().write(true).open(path) {
        Ok(_) => true,
        Err(_) => false,
    }
}

Try / catch

match guarded_op() {
    Err(e) if e.kind() == io::ErrorKind::PermissionDenied || e.kind() == io::ErrorKind::AlreadyExists => {
        // walk e.source() for the native error code, then retry or escalate
    }
    other => other?,
}

Prevention

When it happens

Trigger: Any of the wrapped call sites fails: acquire_named_private_lock (lock contention/permissions), replace_file_checked, move_guarded_file, remove_guarded_file (rename/remove races or denied access) inside the guarded-file layer.

Common situations: File locked by another process or an antivirus scanner during replace/move; insufficient privileges to rename or delete inside a trusted directory; target path in use (ERROR_SHARING_VIOLATION); leftover handles from a crashed process holding the lock.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/b8dc5e5a9aa449ce. Report an issue: GitHub.