When Things Fall Apart: Errors, Resources, Data Structures, and Hashing

By FreePare Team · Wed Aug 05 2026 · 10 min read

When Things Fall Apart: Errors, Resources, Data Structures, and Hashing

Correct code is not only code that returns the right value. It must also release what it acquires, choose a structure that matches its workload, and preserve the contracts on which that structure depends.

This lab tests those three abilities. You will predict behavior, repair a resource-lifetime bug, defend a data-structure choice, and break a hash table on purpose before fixing it.

Learning contract

By the end, you should be able to:

Pin down the language boundary

Do not mix the rules below. Run the examples with one of these baselines and record the exact patch version printed by the tool.

TrackBaselineCheckImportant boundary
CC23 with GCCgcc --versionManual ownership; failures commonly use return values and errno
JavaJava SE 25java --versionExceptions; deterministic cleanup through try-with-resources
PythonCPython 3.14python3.14 --versionExceptions; deterministic cleanup through context managers

The language rules are defined by the C working draft N3220, the Java Language Specification, SE 25, and the Python 3.14 language reference.

Ten-minute baseline

Answer before reading further. Give yourself one point per defensible answer.

1. If fopen succeeds but fclose fails, has the write definitely succeeded?

2. In Java, which exception survives when both the try body and close() throw inside try-with-resources?

3. What does a Python context manager guarantee, and what does it not guarantee about a lazy iterator returned from the block?

4. You need ordered iteration and index access. Which structure fits?

5. You need FIFO removal from both ends. Which structure fits?

6. You need membership checks and do not need duplicates. Which structure fits?

7. What must be true whenever two Java objects are equal?

8. Why can a mutable map key become unreachable even though it is still physically stored?

9. Does a hash-table lookup have an unconditional O(1) guarantee?

10. What input would you use to prove that cleanup runs after a mid-operation failure?

Score 8-10: attempt the exit lab first. Score 5-7: complete the repair sections. Score 0-4: run every example and write the contract of each function in one sentence.

1. Errors are part of the function contract

Every operation has at least two outputs: its useful result and its failure state. A good contract answers four questions:

1. How is success represented?

2. How is failure represented?

3. Who owns resources after success and after failure?

4. Can cleanup itself fail?

C23: one acquisition path, one cleanup path

C does not unwind the stack for you. If a function acquires two resources, every return path must release the resources it owns.

#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

int read_prefix(const char *path, size_t limit, char **out) {
    FILE *file = NULL;
    char *buffer = NULL;
    int status = -1;

    if (path == NULL || out == NULL || limit == 0 || limit == SIZE_MAX) {
        errno = EINVAL;
        return -1;
    }
    *out = NULL;

    file = fopen(path, "rb");
    if (file == NULL) {
        goto cleanup;
    }

    buffer = malloc(limit + 1);
    if (buffer == NULL) {
        goto cleanup;
    }

    size_t read_count = fread(buffer, 1, limit, file);
    if (ferror(file)) {
        goto cleanup;
    }

    buffer[read_count] = '\0';
    *out = buffer;
    buffer = NULL;                 // ownership moves to the caller
    status = 0;

cleanup:
    free(buffer);                  // free(NULL) is safe
    if (file != NULL && fclose(file) != 0) {
        status = -1;
        free(*out);
        *out = NULL;
    }
    return status;
}

The important idea is not goto; it is ownership. The invariant is: every live resource has exactly one current owner and every exit reaches the owner's release path. Before the transfer, the function owns buffer. After *out = buffer, the caller owns it. The assignment buffer = NULL prevents double-free during cleanup.

Compile with warnings and sanitizers:

gcc -std=c23 -Wall -Wextra -Wpedantic -Wconversion \
  -fsanitize=address,undefined -g resource_lab.c -o resource_lab
./resource_lab

Repair challenge: remove buffer = NULL, make the success path execute, and explain the sanitizer report. Then restore the line.

Java 25: cleanup is lexical

Java's try-with-resources closes each declared resource when control leaves the statement, including on an exception or early return. Resources close in reverse declaration order. If the body throws and close() also throws, the close failure is attached as a suppressed exception rather than replacing the primary failure. Those semantics are specified in JLS 14.20.3.

import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

final class PrefixReader {
    static String firstLine(Path path) throws IOException {
        try (BufferedReader reader = Files.newBufferedReader(path)) {
            return reader.readLine();
        }
    }
}

The method does not pretend an I/O failure is an empty line. Its error contract is explicit: callers receive a string, null at end-of-file, or an IOException.

Interviewer follow-up: when should this method translate IOException into a domain exception? Answer: at a boundary that can add useful domain meaning, while preserving the original exception as the cause.

Python 3.14: context managers control a region, not future work

The with statement calls a context manager's exit logic when the controlled block ends (Python reference).

from pathlib import Path


def first_line(path: Path) -> str:
    with path.open(encoding="utf-8") as stream:
        return stream.readline().rstrip("\n")

Now consider a lazy result:

def broken_lines(path: Path):
    with path.open(encoding="utf-8") as stream:
        return (line.strip() for line in stream)

The generator is returned after the block closes the file. The failure appears later, away from the ownership mistake. Either consume the iterator inside the block or make the generator itself own the block:

def lines(path: Path):
    with path.open(encoding="utf-8") as stream:
        for line in stream:
            yield line.strip()

The caller must exhaust or explicitly close the generator to release the file deterministically.

2. Choose a structure from the workload

Calling data a "list of users" does not mean a list is the correct structure. Write the dominant operations first.

Lab 3 of the placement series works the ordering rules behind the first three rows — which item a stack, a queue or a deque makes available next — and Lab 2 works the set-versus-map decision in the fourth and fifth.

NeedStrong defaultExpected operationCost or constraint to remember
Indexed access and ordered scanArray, ArrayList, or Python listAccess by indexMiddle insertion and deletion move elements
LIFO undo or parsingStack, often a dequePush/pop one endDefine behavior on empty input
FIFO schedulingQueue or dequeAdd at tail, remove at headAvoid front removal from an array-backed list
Membership without duplicatesHash setExpected constant-time membershipUses extra memory; collision behavior matters
Key-to-value associationHash mapExpected constant-time lookupKeys need stable equality and hash behavior
Sorted keys and range queriesBalanced ordered map or setLogarithmic search/updateLarger constants and node overhead
Both-end insertion and removalDequeConstant-time end operationsRandom access is not its purpose

The Java APIs document the intended operations for ArrayDeque, HashSet, and HashMap. Python documents list, set, and dictionary behavior in Built-in Types.

Defend five choices

For each profile, name the structure and one rejected alternative.

1. Detect whether a student ID has appeared before; order is irrelevant.

2. Process support tickets in arrival order.

3. Undo editor actions in reverse order.

4. Count how many times each error code occurs.

5. Return all employees with IDs between two values in sorted order.

A complete answer is not "use a map because it is fast." It states operations, ordering, duplicate policy, expected complexity, memory cost, and worst-case risk.

3. Hashing works only while its contracts hold

A hash table computes a bucket from a key's hash, then resolves collisions among keys that reach the same bucket. The expected performance depends on a reasonable distribution and controlled load. It is not an unconditional mathematical promise.

Equality and hash must agree

In Java, equal objects must produce equal hash codes. Unequal objects may still collide. An immutable record makes a safe key when all components are themselves stable:

import java.util.HashMap;
import java.util.Map;

record StudentKey(long studentId, String campus) {}

final class AttendanceIndex {
    public static void main(String[] args) {
        Map<StudentKey, Integer> absences = new HashMap<>();
        StudentKey key = new StudentKey(42L, "Nashik");
        absences.put(key, 3);

        System.out.println(absences.get(new StudentKey(42L, "Nashik"))); // 3
    }
}

Python follows the same practical rule for hashable objects. Frozen data classes are useful key types:

from dataclasses import dataclass


@dataclass(frozen=True)
class StudentKey:
    student_id: int
    campus: str


absences = {StudentKey(42, "Nashik"): 3}
assert absences[StudentKey(42, "Nashik")] == 3

Mutable-key counterexample

This Java class is legal and dangerous:

final class MutableKey {
    String value;

    MutableKey(String value) { this.value = value; }

    @Override public boolean equals(Object other) {
        return other instanceof MutableKey key && value.equals(key.value);
    }

    @Override public int hashCode() { return value.hashCode(); }
}

Insert new MutableKey("A"), change its value to "B", and a lookup may search the bucket for the new hash while the entry remains in the old bucket. The table has not lost the object; your key violated the stability assumption.

The repair is to use immutable keys, or to remove the key before changing equality-relevant state and reinsert it afterward. Immutability is safer.

Collisions and load

Collision handling is normal. A correct table compares candidate keys after reaching a bucket. Too many collisions increase work, and a high load eventually triggers resizing. Exact thresholds and bucket structures are implementation details; do not make a portable design depend on them. The stable contract is the documented API behavior, not a particular internal threshold.

Security follow-up: if attackers control keys, collision patterns can become an availability concern. Treat the hash function and input boundary as part of the threat model, not merely a performance footnote.

Exit lab

Ten points, and the learning contract at the top sets the pass mark at eight. Work from a clean directory, on one of the baselines in the table above, and write the version numbers down before you start.

1. Predict, then run (2 points). Before executing anything, write down what happens when a caller iterates the generator returned by broken_lines. Then run it. One point for the prediction; one for naming the exact line that moves ownership out of the with block.

2. Repair a resource-lifetime bug (3 points). Delete buffer = NULL; from read_prefix, build it with the sanitizer flags in section 1, and run it on a file that exists. Say in one sentence which block is freed before the function returns and why the caller is then holding a dangling pointer; then restore the line and confirm the report is gone. Restoring the line without the explanation scores nothing, because the line is not the lesson — the ownership transfer above it is.

3. Defend a structure choice (3 points). Take three of the five profiles in section 2 and answer each one in the full form that section demands: operations, ordering, duplicate policy, expected complexity, memory cost, and the worst case that would change your mind. One point per profile. "A map, because it is fast" scores zero.

4. Break a hash table on purpose (2 points). Put a MutableKey("A") into a HashMap, change its field to "B", then look the entry up three ways: with the mutated object, with a fresh MutableKey("B"), and with a fresh MutableKey("A"). Record all three results and explain each one from the two contracts in this section rather than from the symptom. One point for the demonstration, one for the explanation.

Below eight, redo only the section the lost points came from and retake after 72 hours. An answer that describes a hash lookup as an unconditional O(1) guarantee cannot pass until it is corrected, whatever the other tasks scored.

Tags: resource-management, programming-fundamentals, coding-practice, coding-interview-patterns, coding-fundamentals, coding-interview-tips, how-to-study-better, how-to-prepare-for-placements