{"record":{"id":"86e6521ca53e3694","repo":"schollz/croc","slug":"choose-at-least-one-file","errorCode":null,"errorMessage":"Choose at least one file","messagePattern":"Choose at least one file","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"web/src/protocol/client.ts","lineNumber":270,"sourceCode":"  error: unknown,\n) {\n  if (!control || !key) return;\n  try {\n    await sendControl(control, {\n      t: \"error\",\n      m: errorMessage(error).slice(0, 500),\n    }, key);\n  } catch {\n    // The connection may already be gone.\n  }\n}\n\nexport async function prepareFiles(\n  selected: File[],\n  callbacks: TransferCallbacks = {},\n  signal?: AbortSignal,\n) {\n  if (selected.length === 0) throw new Error(\"Choose at least one file\");\n  const names = new Set<string>();\n  const outgoingNames = selected.map((file) => normalizeOutgoingFileName(file.name));\n  for (let index = 0; index < selected.length; index += 1) {\n    const file = selected[index];\n    const outgoingName = outgoingNames[index];\n    if (names.has(outgoingName)) throw new Error(`Duplicate filename: ${outgoingName}`);\n    if (!Number.isSafeInteger(file.size)) throw new Error(`File is too large: ${file.name}`);\n    names.add(outgoingName);\n  }\n\n  const prepared: PreparedFile[] = [];\n  const engine = wasm();\n  for (let index = 0; index < selected.length; index += 1) {\n    checkAbort(signal);\n    const file = selected[index];\n    callbacks.onStatus?.(`Hashing ${index + 1}/${selected.length}: ${file.name}`);\n    const hashHandle = await engine.hashInit();\n    const reader = file.stream().getReader();","sourceCodeStart":252,"sourceCodeEnd":288,"githubUrl":"https://github.com/schollz/croc/blob/e25f1bdc04f07f094d50b0a1bf67e2563944b57a/web/src/protocol/client.ts#L252-L288","documentation":"prepareFiles throws immediately when the selected File array is empty. It is a precondition check before any hashing or preparation work begins, ensuring the sender always has at least one file to offer. Cheap and deterministic: it never depends on network or wasm state.","triggerScenarios":"Calling prepareFiles([]) because the file picker returned nothing (user cancelled), or a UI state bug clearing the selection before submit.","commonSituations":"Drag-and-drop handlers that register an empty drop; file input cleared after re-render; programmatic callers passing an unfiltered array.","solutions":["Guard the send action in the UI: disable it until at least one file is selected","If the picker was cancelled, simply return instead of calling prepareFiles"],"exampleFix":"// before\nawait prepareFiles(fileInput.files /* empty after cancel */);\n\n// after\nconst files = [...fileInput.files];\nif (files.length === 0) return; // or show 'Choose at least one file' in the UI","handlingStrategy":"validation","validationCode":"if (!(selected instanceof Array) || selected.length === 0) {\n  throw new Error(\"Choose at least one file\");\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Disable the send button until the selection is non-empty","Treat a cancelled file picker as a no-op, not an error path","Keep selection state and the send handler reading the same source of truth"],"tags":["validation","user-input","files"],"backgroundTag":null,"analyzedSha":"e25f1bdc04f07f094d50b0a1bf67e2563944b57a","analyzedAt":"2026-08-15T12:53:39.096Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}