# Buggy code
def collapsed(self, dims_to_collapse=None):
    """
    Returns a copy of this coordinate, which has been collapsed along
    the specified dimensions.

    Replaces the points & bounds with a simple bounded region.
    """
    import dask.array as da

    # Ensure dims_to_collapse is a tuple to be able to pass
    # through to numpy
    if isinstance(dims_to_collapse, (int, np.integer)):
        dims_to_collapse = (dims_to_collapse,)
    if isinstance(dims_to_collapse, list):
        dims_to_collapse = tuple(dims_to_collapse)

    if np.issubdtype(self.dtype, np.str_):
        # Collapse the coordinate by serializing the points and
        # bounds as strings.
        def serialize(x):
            return "|".join([str(i) for i in x.flatten()])

        bounds = None
        string_type_fmt = "S{}" if six.PY2 else "U{}"
        if self.has_bounds():
            shape = self._bounds_dm.shape[1:]
            bounds = []
            for index in np.ndindex(shape):
                index_slice = (slice(None),) + tuple(index)
                bounds.append(serialize(self.bounds[index_slice]))
            dtype = np.dtype(string_type_fmt.format(max(map(len, bounds))))
            bounds = np.array(bounds, dtype=dtype).reshape((1,) + shape)
        points = serialize(self.points)
        dtype = np.dtype(string_type_fmt.format(len(points)))
        # Create the new collapsed coordinate.
        coord = self.copy(points=np.array(points, dtype=dtype), bounds=bounds)
    else:
        # Collapse the coordinate by calculating the bounded extremes.
        if self.ndim > 1:
            msg = (
                "Collapsing a multi-dimensional coordinate. "
                "Metadata may not be fully descriptive for {!r}."
            )
            warnings.warn(msg.format(self.name()))
        elif not self.is_contiguous():
            msg = (
                "Collapsing a non-contiguous coordinate. "
                "Metadata may not be fully descriptive for {!r}."
            )
            warnings.warn(msg.format(self.name()))

        # Determine the array library for stacking
        al = da if self.has_bounds() and _lazy.is_lazy_data(self.core_bounds()) else np

        item = (
            al.concatenate(self.core_bounds())
            if self.has_bounds()
            else self.core_points()
        )

        # Calculate the bounds and points along the right dims
        bounds = al.stack(
            [item.min(axis=dims_to_collapse), item.max(axis=dims_to_collapse)]
        ).T
        points = al.array(bounds.sum(axis=-1) * 0.5, dtype=self.dtype)

        # Create the new collapsed coordinate.
        coord = self.copy(points=points, bounds=bounds)
    return coord


# Traceback (ValueError)
2.2.0
[warnings about metadata]
Traceback (most recent call last):
File "2d_coord_issue.py", line 24, in <module>
print(cube.collapsed('foo', iris.analysis.MEAN))
File "[site-packages-path]/iris/cube.py", line 3217, in collapsed
collapsed_cube.replace_coord(coord.collapsed(local_dims))
File "[site-packages-path]/iris/cube.py", line 1149, in replace_coord
self.add_aux_coord(new_coord, dims)
File "[site-packages-path]/iris/cube.py", line 949, in add_aux_coord
self._add_unique_aux_coord(coord, data_dims)
File "[site-packages-path]/iris/cube.py", line 981, in _add_unique_aux_coord
data_dims = self._check_multi_dim_metadata(coord, data_dims)
File "[site-packages-path]/iris/cube.py", line 973, in _check_multi_dim_metadata
metadata.shape[i]))
ValueError: Unequal lengths. Cube dimension 0 => 3; metadata 'spanning' dimension 0 => 2.

# Fixed code
def collapsed(self, dims_to_collapse=None):
    """
    Returns a copy of this coordinate, which has been collapsed along
    the specified dimensions.

    Replaces the points & bounds with a simple bounded region.
    """
    import dask.array as da

    # Ensure dims_to_collapse is a tuple to be able to pass
    # through to numpy
    if isinstance(dims_to_collapse, (int, np.integer)):
        dims_to_collapse = (dims_to_collapse,)
    if isinstance(dims_to_collapse, list):
        dims_to_collapse = tuple(dims_to_collapse)

    if np.issubdtype(self.dtype, np.str_):
        # Collapse the coordinate by serializing the points and
        # bounds as strings.
        def serialize(x):
            return "|".join([str(i) for i in x.flatten()])

        bounds = None
        string_type_fmt = "S{}" if six.PY2 else "U{}"
        if self.has_bounds():
            shape = self._bounds_dm.shape[1:]
            bounds = []
            for index in np.ndindex(shape):
                index_slice = (slice(None),) + tuple(index)
                bounds.append(serialize(self.bounds[index_slice]))
            dtype = np.dtype(string_type_fmt.format(max(map(len, bounds))))
            bounds = np.array(bounds, dtype=dtype).reshape((1,) + shape)
        points = serialize(self.points)
        dtype = np.dtype(string_type_fmt.format(len(points)))
        # Create the new collapsed coordinate.
        coord = self.copy(points=np.array(points, dtype=dtype), bounds=bounds)
    else:
        # Collapse the coordinate by calculating the bounded extremes.
        if self.ndim > 1:
            msg = (
                "Collapsing a multi-dimensional coordinate. "
                "Metadata may not be fully descriptive for {!r}."
            )
            warnings.warn(msg.format(self.name()))
        elif not self.is_contiguous():
            msg = (
                "Collapsing a non-contiguous coordinate. "
                "Metadata may not be fully descriptive for {!r}."
            )
            warnings.warn(msg.format(self.name()))

        if self.has_bounds():
            item = self.core_bounds()
            if dims_to_collapse is not None:
                # Express main dims_to_collapse as non-negative integers
                # and add the last (bounds specific) dimension.
                dims_to_collapse = tuple(
                    dim % self.ndim for dim in dims_to_collapse
                ) + (-1,)
        else:
            item = self.core_points()

        # Determine the array library for stacking
        al = da if _lazy.is_lazy_data(item) else np

        # Calculate the bounds and points along the right dims
        bounds = al.stack(
            [item.min(axis=dims_to_collapse), item.max(axis=dims_to_collapse)], axis=-1
        )
        points = al.array(bounds.sum(axis=-1) * 0.5, dtype=self.dtype)

        # Create the new collapsed coordinate.
        coord = self.copy(points=points, bounds=bounds)
    return coord