lovell/sharp · error · Error

Recursive join is unsupported

Error message

Recursive join is unsupported

What it means

Thrown when sharp is asked to join (stack/tile) an array of images while a join is already in progress on the same instance (this.options.joining is already true). Sharp sets a 'joining' flag when it begins processing an array input and recursively builds input descriptors for each element; if one of those elements is itself an array that triggers another join, the nested join cannot be represented and is rejected.

Source

Thrown at lib/input.mjs:99

    inputDescriptor.buffer = Buffer.from(input.buffer, input.byteOffset, input.byteLength);
  } else if (is.plainObject(input) && !is.defined(inputOptions)) {
    // Plain Object descriptor, e.g. create
    inputOptions = input;
    if (_inputOptionsFromObject(inputOptions)) {
      // Stream with options
      inputDescriptor.buffer = [];
    }
  } else if (!is.defined(input) && !is.defined(inputOptions) && is.object(containerOptions) && containerOptions.allowStream) {
    // Stream without options
    inputDescriptor.buffer = [];
  } else if (Array.isArray(input)) {
    if (input.length > 1) {
      // Join images together
      if (!this.options.joining) {
        this.options.joining = true;
        this.options.join = input.map(i => this._createInputDescriptor(i));
      } else {
        throw new Error('Recursive join is unsupported');
      }
    } else {
      throw new Error('Expected at least two images to join');
    }
  } else {
    throw new Error(`Unsupported input '${input}' of type ${typeof input}${
      is.defined(inputOptions) ? ` when also providing options of type ${typeof inputOptions}` : ''
    }`);
  }
  if (is.object(inputOptions)) {
    // failOn
    if (is.defined(inputOptions.failOn)) {
      if (is.string(inputOptions.failOn) && is.inArray(inputOptions.failOn, ['none', 'truncated', 'error', 'warning'])) {
        inputDescriptor.failOn = inputOptions.failOn;
      } else {
        throw is.invalidParameterError('failOn', 'one of: none, truncated, error, warning', inputOptions.failOn);
      }
    }

View on GitHub (pinned to 56676c6918)

Solutions

  1. Flatten the input array to a single level before passing it: sharp(images.flat()).
  2. Restructure so each join is its own sharp instance rather than nesting arrays in one call.
  3. For grid layouts, join row-by-row into intermediate buffers, then join the row results in a second pass.

Example fix

// before
sharp([[a, b], [c, d]], { join: { acro... } })

// after
sharp([a, b, c, d].flat(), { join: { ... } })
Defensive patterns

Strategy: validation

Validate before calling

function flattenJoinInput(maybeNested) {
  if (!Array.isArray(maybeNested)) return maybeNested;
  const flat = maybeNested.flat(Infinity);
  if (flat.length < 2) throw new Error('Join requires at least two images');
  return flat;
}
sharp(flattenJoinInput(maybeNested), { join: {} });

Type guard

function isFlatImageArray(input) {
  return Array.isArray(input) && input.length >= 2 && input.every(i => !Array.isArray(i));
}

Prevention

When it happens

Trigger: Passing a nested array as input to sharp: sharp([[imgA, imgB], [imgC, imgD]]). Also when reusing a sharp instance that is mid-join, or building join inputs where one element resolves to another array via _createInputDescriptor. The flag this.options.joining being truthy on re-entry is the exact trigger.

Common situations: Attempted grid/tile joins where the developer passes a 2D array hoping for rows-and-columns layout (sharp only supports flat 1D joins). Dynamically composing join lists where flattening was forgotten.

Related errors


AI-assisted analysis of lovell/sharp@56676c6918 (2026-08-13). Data as JSON: /api/errors/0ecdc69458e297bb. Report an issue: GitHub.