Back to JavaScript
JavaScript
easy
mid

How would you implement a Fetcher class with get(id) and post(id, x) methods that throw on missing or duplicate ids?

A `Fetcher` with an internal `Map<id, value>`. `get(id)` throws if id absent. `post(id, x)` throws if id already exists; otherwise stores. Tests the basics of class state + invariants. Extensions: `update`/`delete`, custom error types, async simulation, typed generics, eviction.

3 min read·~20 min to think through

A small but precise problem: implement a class with two methods that enforce invariants (no missing reads, no duplicate writes). The interview tests whether you write tight code and handle errors deliberately.

The core implementation

ts
class FetcherError extends Error {
  constructor(message: string, public code: string) {
    super(message);
    this.name = "FetcherError";
  }
}

class Fetcher<T = unknown> {
  private store = new Map<string, T>();

  get(id: string): T {
    if (!this.store.has(id)) {
      throw new FetcherError(`Missing id: ${id}`, "NOT_FOUND");
    }
    return this.store.get(id) as T;
  }

  post(id: string, value: T): void {
    if (this.store.has(id)) {
      throw new FetcherError(`Duplicate id: ${id}`, "DUPLICATE");
    }
    this.store.set(id, value);
  }
}

Why these choices

Map, not plain object

  • O(1) has / get / set for arbitrary keys.
  • Doesn't conflict with prototype keys (__proto__, constructor).
  • Honest iteration order.

Custom error class

Distinguishes the fetcher's domain errors from generic Error. Callers can check err instanceof FetcherError or err.code. Avoid throwing string literals or mixing in arbitrary Error shapes.

Throw vs return

The spec says throw — do that. If the caller wants a "soft" API, layer it:

js
function tryGet(fetcher, id) {
  try { return { ok: true, value: fetcher.get(id) }; }
  catch (e) { return { ok: false, error: e }; }
}

Extensions to mention

update / delete

ts
update(id: string, value: T): void {
  if (!this.store.has(id)) throw new FetcherError(`Missing id: ${id}`, "NOT_FOUND");
  this.store.set(id, value);
}

delete(id: string): void {
  if (!this.store.has(id)) throw new FetcherError(`Missing id: ${id}`, "NOT_FOUND");
  this.store.delete(id);
}

Async variant

If the spec is "simulate a remote fetcher":

ts
async get(id: string): Promise<T> {
  await delay(20);
  if (!this.store.has(id)) throw new FetcherError(`Missing id: ${id}`, "NOT_FOUND");
  return this.store.get(id) as T;
}

TTL / eviction

ts
private timers = new Map<string, NodeJS.Timeout>();
post(id: string, value: T, ttlMs?: number) {
  if (this.store.has(id)) throw new FetcherError(...);
  this.store.set(id, value);
  if (ttlMs) {
    this.timers.set(id, setTimeout(() => this.store.delete(id), ttlMs));
  }
}

upsert

A separate method, not a flag — different semantics.

Tests to mention

  • get on missing → throws specific error.
  • post on duplicate → throws.
  • post then get → returns the stored value.
  • post → update → get → returns new value.
  • ids must be the right type (TS catches this; runtime — guard).

Pitfalls

  • Throwing strings instead of Error subclasses — loses stack traces.
  • Using plain object {} as store — prototype pollution.
  • Returning undefined on missing instead of throwing — violates the spec.
  • Mutating value after post — caller reference still mutates the internal copy; if defensive, clone.

Interview framing

"A class with a Map<id, value> internal store. get throws a custom error if the id isn't present; post throws if it already is. Map (not plain object) for O(1) has/get/set and to avoid prototype-key conflicts. Custom error class so callers can distinguish domain errors from generic Error. Typed generic over the value. Extensions to discuss if asked: update/delete, async variant simulating a remote fetcher, TTL/eviction, and a soft-API wrapper that returns {ok, value/error} instead of throwing for callers who want it."

Follow-up questions

  • Why use Map instead of a plain object?
  • Why a custom error class?
  • How would you make it async?
  • How would you add TTL/eviction?

Common mistakes

  • Plain object store with prototype-key risk.
  • Throwing strings.
  • Returning undefined on missing instead of throwing.
  • No type safety on the value.

Performance considerations

  • O(1) operations via Map. For very high cardinality, watch memory; add eviction.

Edge cases

  • post with the same id and same value — still throw (spec says duplicate).
  • get/post on empty-string id — should reject if invalid?
  • Concurrent calls (in async variant).

Real-world examples

  • In-memory cache primitive, test doubles for an API client.

Senior engineer discussion

Seniors use Map, throw typed errors, and decide explicitly between throwing and returning result objects — they don't mix.

Related questions