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

  1. Log the received type to identify the stray message and its origin in the sender's flow
  2. Verify the sender's loop sends finished immediately upon receiving the recipient's finished (line 474-477 of the sender path)
  3. Upgrade both peers so termination choreography matches
  4. 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

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


AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15). Data as JSON: /api/errors/9814a13dbf2aa560. Report an issue: GitHub.