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; // 30 seconds
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:
// Connect with timeout
await withTimeout(ndk.connect(), NDK_TIMEOUT_MS, "NDK connect");
// Fetch events with timeout
const events = await withTimeout(
ndk.fetchEvents({ kinds: [0], authors: [pubkey] }),
NDK_TIMEOUT_MS,
"fetch profile"
);
// Publish with timeout
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