can1357/oh-my-pi · error · Error

native task ` ` panicked

Error message

native task `{tag}` panicked: {message}

What it means

The Blocking napi Task runs work on libuv's thread pool; because that thread crosses an extern "C" FFI boundary, a Rust panic must not escape (it would force-abort the host). The task wraps work in catch_unwind and, if the closure panicked, converts the panic into a napi GenericFailure Error whose message names the task tag and the panic message. The JS promise rejects with this error instead of the process crashing.

Solutions

  1. Read the embedded panic message after 'panicked: ' to identify the failing native operation and cause
  2. Check the input you passed (sizes, encodings, null/undefined fields) against the function's documented contract
  3. Retry once to rule out transient state, then report the input as a native-module bug with the panic message
  4. Update the native module — panics here are usually fixed upstream as bugs

Example fix

// before
const result = await native.someBlockingTask(buf);
// after
let result;
try {
  result = await native.someBlockingTask(buf);
} catch (err) {
  if (String(err?.message).startsWith('native task `someBlockingTask` panicked')) {
    result = null; // fall back to JS implementation
  } else throw err;
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (buf != null && typeof buf !== 'string' && !(buf instanceof Uint8Array)) throw new TypeError('expected string or Uint8Array');

Type guard

const isNativeInput = (v) => typeof v === 'string' || v instanceof Uint8Array;

Try / catch

try {
  return await native.someBlockingTask(input);
} catch (err) {
  const msg = String(err?.message ?? err);
  if (msg.startsWith('native task `someBlockingTask` panicked')) {
    log.error('native panic', { panic: msg.split('panicked: ')[1] });
    return jsFallback(input);
  } throw err;
}

Prevention

When it happens

Trigger: Any blocking native operation dispatched through the Blocking task whose closure panics — e.g. slice index out of bounds, unwrap on None/Err, assert failure, or an explicit panic inside the native implementation while processing your input.

Common situations: Feeding malformed/unexpected input to a native function (malformed buffer, invalid UTF-8 boundary); a native bug triggered by an edge case; builds with different overflow-check settings than CI.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/7dbbb71b184d77ee. Report an issue: GitHub.

Appendix: source

Thrown at crates/pi-natives/src/task.rs:197

		let tag = self.tag;
		// Guard the napi-rs async-work FFI boundary. `execute` is registered as
		// a plain `unsafe extern "C" fn` (napi 3.9.4 `src/async_work.rs:109`),
		// so an unwind escaping this frame would cross a non-`C-unwind` FFI
		// edge and force-abort the host under Rust's stabilized C-unwind rules
		// (RFC 2945, stable since 1.81). The crash handler scope tells the
		// global panic hook this panic is about to be caught and mapped to a
		// `GenericFailure`, so it downgrades the report to a disk-only crash
		// log — no stderr dump, no default-hook chaining.
		match catch_unwind(AssertUnwindSafe(move || {
			crate::crash_handler::blocking_task_panic_scope(move || work(cancel_token))
		})) {
			Ok(result) => result,
			Err(payload) => {
				// Extract the message BEFORE touching the payload's destructor:
				// disposal is the one remaining step that can panic again.
				let message = crate::crash_handler::panic_payload(&*payload);
				dispose_panic_payload(payload);
				Err(Error::new(
					Status::GenericFailure,
					format!("native task `{tag}` panicked: {message}"),
				))
			},
		}
	}

	fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
		Ok(output)
	}
}

/// Dispose of a caught panic payload without any possibility of a second
/// unwind escaping this frame.
///
/// A [`std::panic::panic_any`] payload is an arbitrary user type whose `Drop`
/// impl may itself panic. [`Blocking::compute`] runs inside napi's async-work
/// `extern "C"` frame, so a panic escaping the payload's destructor would

View on GitHub (pinned to 9690622007)