schollz/croc · error · Error
Expected transfer completion, got ${finishedMessage.t}
Error message
Expected transfer completion, got ${finishedMessage.t} What it means
Thrown by the recipient after sending its final 'finished' message when the sender does not echo 'finished' back. This final exchange confirms both sides consider the whole transfer complete; any other message (or a stale frame) means the sender's overall state does not match — it may still be in a file loop, have reported an error earlier, or the control channel delivered unexpected data. The transfer is aborted after all files were received and committed.
Source
Thrown at web/src/protocol/client.ts:815
if (close.t === "error") throw new Error(close.m || "Sender cancelled");
if (close.t !== "close-recipient") {
throw new Error(`Expected sender to close the file, got ${close.t}`);
}
callbacks.onStatus?.(`Verifying ${file.path}`);
await verifySink(sink, file.hash);
await sink.commit();
totalTransferred = beforeFile + file.size;
callbacks.onFileComplete?.(file.path);
} catch (error) {
await sink.abort();
throw error;
}
}
await sendControl(control, { t: "finished" }, key);
const finishedMessage = await receiveControl(control, key);
if (finishedMessage.t !== "finished") {
throw new Error(`Expected transfer completion, got ${finishedMessage.t}`);
}
callbacks.onStatus?.("Transfer complete");
} catch (error) {
await reportPeerError(control, key, error);
throw error;
} finally {
receiver?.stop();
closeAll(control, data);
}
}
View on GitHub (pinned to e25f1bdc04)
Solutions
- Log the received type to identify the stray message and its origin in the sender's flow
- Verify the sender's loop sends finished immediately upon receiving the recipient's finished (line 474-477 of the sender path)
- Upgrade both peers so termination choreography matches
- Retry with a fresh code phrase; room collisions can inject foreign frames at completion time
Defensive patterns
Strategy: try-catch
Type guard
function isUnexpectedCompletion(e: unknown): boolean {
return e instanceof Error && e.message.startsWith("Expected transfer completion");
} Try / catch
catch (e) {
if (e instanceof Error && e.message.startsWith("Expected transfer completion")) {
// files were committed but the final handshake failed: treat data as delivered, log the stray type
}
throw e;
} Prevention
- If implementing a sender, always echo finished immediately upon receiving finished
- Note that this error fires AFTER all files were verified and committed — your data is safe; report it as a protocol anomaly, not data loss
- Use fresh code phrases per transfer to avoid foreign frames at completion time
When it happens
Trigger: Sender sends an extra control message (e.g. another recipientready or error) instead of finished; relay delivers a duplicated stale frame after the last file; sender implementation omits the final finished echo; desynchronized message counters between peers.
Common situations: Version-skewed sender with a different termination sequence; mock senders that skip the final handshake; relay frame duplication at connection teardown; a race where the sender's error report from the last file arrives after the recipient already moved on.
Related errors
- Unexpected peer message: ${message.t}
- Expected recipient to close the file, got ${closed.t}
- Received file data before it was requested
- Expected sender to close the file, got ${close.t}
- Relay returned an invalid port list: ${banner}
AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15).
Data as JSON: /api/errors/9814a13dbf2aa560.
Report an issue: GitHub.