| name | ndk-operation-timeout-wrapper |
| description | Fix NDK (Nostr Dev Kit) operations hanging indefinitely when relay connections stall.
Use when: (1) App freezes during fetchEvents or publish calls, (2) No timeout errors
despite network issues, (3) Relay connection appears stuck, (4) Using NDK with unstable
or slow relays. NDK operations have no built-in timeout - wrap with Promise.race.
|
| author | Claude Code |
| version | 1.0.0 |
| date | "2026-01-31T00:00:00.000Z" |
NDK Operation Timeout Wrapper
Problem
NDK (nostr-dev-kit) operations like fetchEvents() and ndkEvent.publish() have no
built-in timeout. When relay connections stall or become unresponsive, these operations
hang indefinitely, causing the application to freeze without any error message.
Context / Trigger Conditions
- Application freezes during Nostr operations
- No timeout error thrown despite minutes of waiting
- Works sometimes, hangs randomly (relay-dependent)
- Log shows operation started but never completes
- Using NDK with multiple relays where some may be unreliable
Solution
Create a timeout wrapper function:
const NDK_TIMEOUT_MS = 30000;
async function withTimeout<T>(
promise: Promise<T>,
ms: number,
operation: string
): Promise<T> {
let timeoutId: ReturnType<typeof setTimeout>;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(
() => reject(new Error(`${operation} timed out after ${ms}ms`)),
ms
);
});
try {
const result = await Promise.race([promise, timeoutPromise]);
clearTimeout(timeoutId!);
return result;
} catch (error) {
clearTimeout(timeoutId!);
throw error;
}
}
Wrap all NDK operations:
await withTimeout(ndk.connect(), NDK_TIMEOUT_MS, "NDK connect");
const events = await withTimeout(
ndk.fetchEvents({ kinds: [0], authors: [pubkey] }),
NDK_TIMEOUT_MS,
"fetch profile"
);
const relaySet = NDKRelaySet.fromRelayUrls(relayUrls, ndk);
await withTimeout(
ndkEvent.publish(relaySet),
NDK_TIMEOUT_MS,
"relay publish"
);
Also ensure timeout errors are retryable:
function isRetryableError(error: unknown): boolean {
if (error instanceof Error) {
const message = error.message.toLowerCase();
const errorName = error.name.toLowerCase();
if (
message.includes("timeout") ||
message.includes("aborted") ||
errorName.includes("timeout") ||
errorName.includes("abort")
) {
return true;
}
}
return false;
}
Verification
After implementing, operations that previously hung should now:
- Throw a timeout error after the specified duration
- Allow retry logic to attempt the operation again
- Log the specific operation that timed out
Example
Before (hangs forever):
const events = await ndk.fetchEvents({ kinds: [34236], "#d": [vineId] });
After (times out and can retry):
const events = await withTimeout(
ndk.fetchEvents({ kinds: [34236], "#d": [vineId] }),
30000,
"check video exists"
);
Notes
- 30 seconds is a reasonable default; adjust based on expected operation duration
- Consider shorter timeouts for existence checks, longer for batch operations
- Wrap ALL NDK operations, not just problematic ones (any can hang)
- This pattern applies to any async library without built-in timeouts
- For AbortController support (if library supports it), prefer that over Promise.race
References