# fast-copy, a deep object copier built around a cache and per-type methods

> fast-copy is a JavaScript deep copy library that treats copying as a traversal with a cache and a table of per-type methods, so maps, sets, typed arrays, blobs, dates and regexps each get their own handler. A createCopier factory lets you override those methods, swap the cache, or change the depth limit, and copyStrict exists for objects where property descriptors and non-enumerable keys matter.

**planttheidea/fast-copy** — A blazing fast deep object copier

- Repository: https://github.com/planttheidea/fast-copy
- Stars: 1,158 · Forks: 31
- Language: JavaScript
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/planttheidea-fast-copy

## Copy, and check that it is not the same object

The library is small and the usage example is small with it. You import copy from fast-copy, optionally bring in deepEqual from fast-equals as the verification side, and then check two things about the result: that the copy is not the same object reference, and that it is deeply equal to the original. That second assertion is the one worth internalising, because a deep copy that is not deeply equal is worse than no copy. The object in the example deliberately mixes an array containing a nested object with a Map holding both a plain key and an object key, because those are the cases where a copier is either careful or broken. The package is MIT licensed, the build badge in the project page reports passing, and the coverage badge claims 100 percent, which for a library whose whole job is a long list of type-specific branches is a meaningful claim rather than a decorative one.

```js
import { copy } from 'fast-copy';
import { deepEqual } from 'fast-equals';

const object = {
  array: [123, { deep: 'value' }],
  map: new Map([
    ['foo', {}],
    [{ bar: 'baz' }, 'quz'],
  ]),
};

const copiedObject = copy(object);
```

## The three places the default copier is deliberately not a copy

The documentation has a section on the aspects of the default copiers, and it names three behaviours that will surprise you if you assume a copy is a faithful mirror. Error references are copied directly, instead of creating a new Error object, so the copy shares the original error instance. The constructor of the original object is used, instead of using known globals, which is what makes custom classes survive a copy intact rather than being flattened into plain objects. And sparse arrays are copied densely, unless you use copyStrict, so an array with holes comes back without them. Each of these is a defensible choice for a performance-oriented copier, and each is a place where a bug report from a user will start with the same sentence. The third is the one most likely to bite, because sparse arrays are rare in application code and therefore rarely tested, and the second is the one most likely to be a feature you wanted.

```
Error references are copied directly, instead of creating a new `*Error` object
The constructor of the original object is used, instead of using known globals
Sparse arrays are copied densely, unless using `copyStrict`
```

## copyStrict, and the price of fidelity

copyStrict is the other half of the API, and its list of differences tells you exactly what fidelity costs. Properties retain their original property descriptor, non-enumerable keys are copied, non-standard properties such as keys on arrays, dates, maps and sets are copied, and holes in sparse arrays are preserved. The documentation is blunt about the tradeoff: this method is significantly slower than copy, so it is recommended to only use this when you have specific use cases that require it. The example shows the case in question, defining a non-enumerable property with defineProperty and then copying it, which is exactly the situation the default copier would silently drop. A library that offers both modes and says which one is faster and which one is lossless is doing you a favour, because most deep copy utilities offer neither choice nor an explanation.

```js
import { copyStrict } from 'fast-copy';

const object = { foo: 'bar' };
Object.defineProperty(object, 'bar', {
  enumerable: false,
  value: 'baz',
});

const copied = copyStrict(object);
```

## The type table, which is the library in miniature

The methods option is a table mapping a copy method to the types it handles, and reading it is the fastest way to understand what the library actually supports. The array method handles Array. The arrayBuffer method covers ArrayBuffer and every typed array variant, from Float32Array and Float64Array through the Int and Uint families to BigInt64Array and BigUint64Array. The blob method handles Blob, the dataView method handles DataView, the date method handles Date, the error method handles Error and its subclasses including AggregateError, EvalError, RangeError, ReferenceError, SyntaxError, TypeError and URIError, the map and set methods handle Map and Set, the object method handles Object or any custom constructor, and the regExp method handles RegExp. Every method has the same contract, a function from value and state to value, where state carries the constructor, the cache, the copier itself and the prototype.

```js
type InternalCopier<Value> = (value: Value, state: State) => Value;

interface State {
  Constructor: any;
  cache: WeakMap;
  copier: InternalCopier<any>;
  prototype: any;
}
```

## The cache is how circular references work

The state object carries a cache, which defaults to a new WeakMap, and that cache is the mechanism behind circular reference handling. The documentation says it directly: if you want to maintain circular reference handling, then you will need the methods to handle cache population for future lookups, and the example shows the shape of a custom array method that clones the array and then calls state.cache.set with the original value and the clone. So the cache is a two-way map from original to clone, and a copier that wants circular references correct has to write into it. That is a real design cost for anyone writing a custom method, and it is why the createCache option exists, letting you swap the WeakMap for something else if your use case needs it. The depth count, incidentally, counts nested objects only, and primitives and values already present in the cache do not contribute to it.

```js
function shallowlyCloneArray<Value extends any[]>(
  value: Value,
  state: State
): Value {
  const clone = [...value];

  state.cache.set(value, clone);

  return clone;
}
```

## The depth limit, and why it is a RangeError subclass

The maxDepth option is the most interesting engineering decision in the library, because it converts a crash into a catchable error. The maximum number of nested objects traversed before a MaxDepthExceededError is thrown defaults to 1000, and the reason is stated plainly: because copying is recursive, a value nested more deeply than the engine's call stack allows will exhaust the stack, and the default limit sits below that threshold so deeply nested values fail with a descriptive, catchable error instead of a raw RangeError. MaxDepthExceededError extends RangeError, so existing handling of the native stack exhaustion error continues to work, and the error carries the limit that was exceeded. The 1000 default assumes the stack available to a standard Node.js or browser main thread, where the native limit falls somewhere between roughly 1800 and 4000 levels depending on the copier and the JIT state, and environments with a smaller stack, such as a worker started with a reduced stackSizeMb, can exhaust it sooner.

```js
import { copy, MaxDepthExceededError } from 'fast-copy';

try {
  copy(untrustedPayload);
} catch (error) {
  if (error instanceof MaxDepthExceededError) {
    // `error.maxDepth` is the limit that was exceeded
  }
}
```

## Building a custom copier, and the trade it asks for

createCopier builds a copier from type-specific method overrides and configuration options, and the documentation says it is useful if you want to squeeze out maximum performance or perform something other than a standard deep copy. The example is instructive because it is deliberately shallow: the array method spreads the array, the map method copies the entries, the object method spreads the object, the set method copies the values, and the cache is an LRUCache rather than a WeakMap, with a maxDepth of 32 and strict enabled. The result is a copier that does not recurse at all, which is only correct for flat structures, and that is the point: the library hands you the dispatch table rather than making you fork the whole thing. You can also set a custom depth limit per copier, and pass Infinity to remove the limit entirely, in which case sufficiently deep values will again throw a native RangeError.

```js
import { createCopier } from 'fast-copy';
import { LRUCache } from 'lru-cache';

const copyShallowStrict = createCopier({
  createCache: () => new LRUCache(),
  maxDepth: 32,
  methods: {
    array: (array) => [...array],
    map: (map) => new Map(map.entries()),
    object: (object) => ({ ...object }),
    set: (set) => new Set(set.values()),
  },
  strict: true,
});
```

## The package, and the benchmark table

The package is TypeScript in the repository and ships as a build with three module formats, with an ES module entry, a CommonJS entry and a UMD bundle for the browser, each with its own type declarations, and the published files are the dist directory, the LICENSE, the README, an index.d.ts and the package.json. The development dependencies reveal how the performance claim is tested. There is tinybench, the benchmark runner, and the competing implementations the project measures against: lodash, ramda, clone, deepclone and fast-clone, plus react and react-dom, which is where a benchmark of copying state objects would come from. The benchmark directory in the tree and the README table of contents confirm the structure of the results, with separate sections for simple objects, complex objects, big data, circular objects and special objects, which is a sensible way to separate the cases where a copier wins from the ones where it does not. The releases are frequent, v4.2.0 on 2026-10-14, v4.1.2 on 2026-10-05 and v4.1.1 on 2026-09-02.

```
Simple objects
Complex objects
Big data
Circular objects
Special objects
```

## Conclusion

Adopt fast-copy if you are copying non-trivial object graphs in a hot path and the default structuredClone or a JSON round trip is not good enough, and skip it if your objects are plain and shallow, because the built-in alternatives are free and the special type handling only earns its keep on mixed graphs. Verify three things before you rely on it: the behaviour of the default copier on errors, sparse arrays and custom constructors, since the documentation lists three cases where it deliberately differs from a naive copy; the depth limit in your environment, since the default of 1000 assumes a standard main thread and a worker with a reduced stack can exhaust sooner; and that you are on the version that has the MaxDepthExceededError, since catching a descriptive error is different from catching a raw RangeError.

## FAQ

### What does the fast-copy library do?

It deeply copies the object passed, and can also copy in strict mode with copyStrict, which retains original property descriptors, copies non-enumerable keys and non-standard properties, and preserves holes in sparse arrays. It also exports a createCopier factory for custom type-specific copy methods and configuration.

### Which object types does fast-copy handle?

There is a method per type: array for Array, arrayBuffer for ArrayBuffer and the typed array variants, blob for Blob, dataView for DataView, date for Date, error for Error and its subclasses, map for Map, object for Object or any custom constructor, regExp for RegExp, and set for Set.

### What is the maxDepth option in fast-copy and why does it exist?

It is the maximum number of nested objects traversed before a MaxDepthExceededError is thrown, defaulting to 1000. Because copying is recursive, a value nested deeper than the engine's call stack allows would exhaust the stack, so the default limit sits below that threshold and gives a descriptive, catchable error instead of a raw RangeError. MaxDepthExceededError extends RangeError.

### How does fast-copy handle circular references?

Through the cache held in the copier state, which defaults to a new WeakMap. Custom copy methods are responsible for populating it, calling state.cache.set with the original value and its clone, so that later lookups resolve to the same clone.

## Sources

- [Issues](https://github.com/planttheidea/fast-copy/issues)
- [License: MIT](https://github.com/planttheidea/fast-copy/blob/main/LICENSE)
- [planttheidea/fast-copy on GitHub](https://github.com/planttheidea/fast-copy)
- [README](https://github.com/planttheidea/fast-copy/blob/main/README.md)
- [Releases](https://github.com/planttheidea/fast-copy/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/planttheidea-fast-copy
