Representative interview topic

Coding interview: How would you use JavaScript explicit resource management safely?

CodingHard
Offer.cc Editorial TeamPublished Updated

Question

A request handler opens a file, a timer, and an async lock. Show how you would make every resource release on success, failure, and cancellation using Symbol.dispose, Symbol.asyncDispose, using, and await using. Explain scope, disposal order, error handling, feature detection, and tests.

Prompt and scope

The handler acquires synchronous and asynchronous resources whose lifetimes must end at a block boundary. Use JavaScript explicit resource-management protocols to design a safe wrapper, then explain what happens when construction, body execution, or cleanup fails. The answer should distinguish language syntax from runtime and library support; TypeScript 5.2 can type-check the syntax, while production availability still depends on the target engine and transpilation strategy.

This is a coding question because the core skill is resource-lifetime reasoning and failure-safe implementation, not a framework choice.

What interviewers assess

First, can you define a resource with [Symbol.dispose]() for synchronous cleanup and [Symbol.asyncDispose]() for cleanup that must be awaited?

Second, do you understand that using and await using are scoped declarations? Cleanup runs when control leaves the containing block, including exceptions; a top-level long-lived scope is usually the wrong lifetime.

Third, can you state disposal order? Resources are disposed in reverse declaration order, so dependent resources should be declared after the resources they depend on.

Fourth, can you reason about errors? A body error and a disposal error may both need to be preserved as a suppressed error chain; cleanup must not silently replace the primary failure.

Fifth, can you provide a compatibility plan? Feature detection, a compiler transform, or an explicit try/finally adapter may be required when the deployment runtime does not implement the protocol.

Questions to clarify first

  • Which Node.js or browser versions execute the code, and is transpilation allowed?
  • Which resources are synchronous, and which cleanup operations return promises?
  • Does cancellation close the resource immediately, or may in-flight work finish?
  • Are resources independent, or does one cleanup depend on another still being open?
  • Must cleanup errors fail the request, be reported, or be attached to the primary error?
  • Can the code use DisposableStack or only the basic symbols?

30-second answer framework

“I would give each resource an explicit disposal protocol, declare it inside the smallest block that owns its lifetime, and use await using whenever cleanup is asynchronous. I would declare dependencies later so reverse-order disposal is safe, test success, body failure, acquisition failure, and cancellation, and preserve both body and cleanup errors. Before shipping, I would verify engine support or compile to an equivalent try/finally; syntax support in TypeScript does not guarantee runtime support.”

Step-by-step answer

Step 1: Define narrow disposal protocols

Keep acquisition and cleanup together. A synchronous resource exposes [Symbol.dispose]() and must finish cleanup before the block exits. An asynchronous resource exposes [Symbol.asyncDispose]() and is acquired with await using so the generated exit path awaits it.

ts
class FileLease {
  constructor(private readonly fd: number) {}
  [Symbol.dispose]() { closeFile(this.fd) }
}

class AsyncLockLease {
  constructor(private readonly release: () => Promise<void>) {}
  async [Symbol.asyncDispose]() { await this.release() }
}

The methods should be idempotent if a caller can also cancel explicitly. Never return a promise from [Symbol.dispose](); use the async protocol for awaited work.

Step 2: Keep the lifetime block small

Acquire a lease only after entering the scope that owns it. Avoid storing a using variable in a longer-lived object; its cleanup is tied to the lexical block, not to garbage collection.

ts
async function handle() {
  {
    using file = openFileLease()
    await using lock = await acquireLockLease()
    await writeWithLock(file, lock)
  }
}

Here the lock is declared after the file, so the lock releases first and the file closes second. If the body throws, both exits still run.

Step 3: Handle acquisition and cancellation

Acquire resources sequentially or register them in a stack as soon as acquisition succeeds. If a later acquisition fails, already-acquired resources must still be disposed. Connect an abort signal to the operation, but keep disposal in the scope so cancellation cannot bypass cleanup.

Step 4: Preserve failure information

Test a body exception and a cleanup exception separately, then together. The runtime can represent a cleanup failure as suppressed by the primary error; logging should include the complete chain. If an adapter uses try/finally, explicitly attach cleanup failures instead of overwriting the body error.

Step 5: Plan compatibility

Check the actual deployment engine, not only the TypeScript compiler. When native syntax or symbols are unavailable, compile to try/finally, use a vetted polyfill, or wrap resources in an application-level disposable helper. Keep the fallback semantics identical: reverse order, once-only cleanup, awaited async release, and preserved errors.

Step 6: Test lifecycle boundaries

Use deterministic fakes that record acquisition and disposal events. Cover normal return, body throw, second acquisition failure, abort during work, disposal failure, and repeated disposal. Assert event order and that no resource remains open after the promise settles.

Model answer

“I model each handle as a disposable lease. Synchronous handles implement [Symbol.dispose]; asynchronous releases implement [Symbol.asyncDispose]. I create them inside the smallest owning block, use await using for the lock, and declare the lock after the file so reverse-order cleanup respects the dependency. The scope exits on return, throw, and cancellation, so cleanup is guaranteed by the language protocol.

I test acquisition failure, body failure, cleanup failure, and their combination, preserving the primary error plus suppressed cleanup information. I also verify idempotence and abort behavior. Finally, I check the production engine: TypeScript 5.2 support is compile-time help, not proof that the runtime implements the symbols. If support is missing, I transpile or use an explicit try/finally adapter with the same ordering and error semantics.”

Common mistakes

  • Putting a resource in a long-lived scope → cleanup is delayed → bind it to the smallest owning block.
  • Using using for async cleanup → a promise may be ignored → implement and use [Symbol.asyncDispose] with await using.
  • Declaring a dependency first → reverse order closes it too early → declare dependents later.
  • Assuming TypeScript support means runtime support → production fails at parse or symbol lookup → check the engine or compile fallback.
  • Overwriting a body error with cleanup failure → the root cause is lost → preserve the primary error and suppressed cleanup details.
  • Allowing double release → cleanup becomes unsafe → make disposal idempotent or guard it.
  • Testing only success → failure paths leak resources → test acquisition, body, cancellation, and disposal errors.

Follow-up questions

Follow-up 1: Does using replace garbage collection?

No. It gives deterministic scope-based cleanup for resources such as handles and locks; memory reclamation remains the runtime's job.

Follow-up 2: Why is disposal reverse ordered?

Later declarations commonly depend on earlier ones. Reverse order lets the dependent release before its dependency closes.

Follow-up 3: When should cleanup be asynchronous?

Use the async protocol when release itself needs an awaited operation, such as flushing or returning a lease to a remote coordinator.

Follow-up 4: What if a body error and cleanup error both occur?

Preserve the body error as primary and expose cleanup failure through the runtime's suppressed-error mechanism or an equivalent explicit error chain.

Follow-up 5: Can a resource be disposed manually too?

Yes, but make the operation idempotent or coordinate ownership so scope exit does not release it twice.

Follow-up 6: What is the fallback without native support?

Use a compiler transform, a vetted polyfill, or a small try/finally adapter that preserves reverse order, awaiting, once-only cleanup, and error chaining.

Public sources

Related questions

Related interview tool

Use Screenshot for a coding prompt

Capture the problem, then work through the constraints, solution, code, edge cases, and complexity in order.

View the tool