gitbutlerapp/gitbutler · error · napi::Error

{e:#}

Error message

{e:#}

What it means

Generated by the `but_api` macro for async API functions: any `anyhow::Error` escaping the function body is converted into a napi generic-failure Error. The message prefers a custom error-chain context message, falling back to the anyhow alternate formatting `{e:#}` (message plus `: cause` chain). It is the generic transport of all internal Rust errors to JavaScript.

Solutions

  1. Read the full `{e:#}` chain in the message to find the root cause.
  2. Validate inputs on the JS side (ids, refs, paths) before calling the API.
  3. Match on the message/context in JS and handle known cases (not-found, invalid-id) explicitly.
  4. If the message lacks context, add `.context(...)` at the failing Rust call site for better surfacing.
Defensive patterns

Strategy: try-catch

Try / catch

try {
  return await api.someAsyncOperation(arg);
} catch (e) {
  const chain = String(e?.message ?? e);
  if (chain.includes('not found')) return null;      // handle known domain cases
  if (chain.includes('invalid')) throw new InputError(chain);
  throw e; // unknown internal failure
}

Prevention

When it happens

Trigger: Any async `but_api`-annotated function returning Err — e.g. git operation failures, file IO errors, invalid project ids — surfacing through the N-API boundary.

Common situations: Calling the desktop/lite SDK from JS with a bad commit id, missing repo state, or after an external git command mutated the worktree; underlying library errors bubble up verbatim including their cause chain.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/4f76357ac6f04433. Report an issue: GitHub.

Appendix: source

Thrown at crates/but-api-macros/src/lib.rs:303

    };

    // Build the napi function body.
    // For async functions, we use an `async {}` block; for sync, spawn_blocking.
    // Both return anyhow::Result to handle any error type uniformly.
    let napi_body = if asyncness.is_some() {
        quote! {
            let __napi_body_result: ::anyhow::Result<::serde_json::Value> = async {
                let result = #napi_call_fn_args?;
                #convert_to_json_result_type
                Ok(::serde_json::to_value(result)?)
            }.await;
            __napi_body_result.map_err(|e: ::anyhow::Error| {
                let ctx = but_error::AnyhowContextExt::custom_context_or_error_chain(&e);
                let message = ctx
                    .message
                    .map(|m| m.to_string())
                    .unwrap_or_else(|| format!("{e:#}"));
                napi::Error::new(napi::Status::GenericFailure, message)
            })
        }
    } else {
        // For sync functions, param conversions must be inside spawn_blocking
        // so that non-Send types (e.g. Context with Rc) are never moved across threads.
        quote! {
            ::tokio::task::spawn_blocking(move || {
                #(#napi_param_conversions);*
                let __napi_body_result: ::anyhow::Result<::serde_json::Value> = (|| {
                    let result = #napi_call_fn_args?;
                    #convert_to_json_result_type
                    Ok(::serde_json::to_value(result)?)
                })();
                __napi_body_result.map_err(|e: ::anyhow::Error| {
                    let ctx = but_error::AnyhowContextExt::custom_context_or_error_chain(&e);
                    let message = ctx
                        .message
                        .map(|m| m.to_string())

View on GitHub (pinned to 58e5313667)