Common Contact List Bugs and How to Catch Them

Common Contact List Bugs and How to Catch Them

June 14, 2026 · 16 min read · Common Issues

Common Contact List Bugs and How to Catch Them

Contact lists are a core feature of almost every mobile and web application. When they fail, users experience frustration, missed communications, and a loss of trust in the product. This guide walks through the most frequent contact‑list defects, explains why they appear, shows how they manifest to real people, and gives concrete steps to reproduce, detect, fix, and prevent each issue. The article also demonstrates how persona‑driven autonomous exploration (the approach behind SUSA) surfaces bugs that scripted tests often miss, and provides a practical test matrix you can adopt immediately.

---

Why Contact List Defects Demand Attention

A contact list is more than a simple table of names and numbers. It is the gateway to messaging, calling, emailing, and often to deeper workflows such as sharing, emergency alerts, or payment verification. A defect in this entry point can cascade: a duplicated entry may cause a user to send a message to the wrong person; a missing country code can break international dialing; an inaccessible label can block users who rely on screen readers. Because the list is touched by many modules—UI, data persistence, sync, permissions, localization—bugs tend to hide in the seams between those layers.

Detecting these problems early saves rework, reduces support tickets, and protects brand reputation. The sections below break down twelve common patterns, each with a cause‑symptom‑fix narrative, reproducible steps, and preventive measures.

---

Common Contact List Bugs and How to Catch Them: Duplicate Entries

How Duplicates Arise

Duplicates typically appear when the app creates a new contact record without checking for an existing match, or when two data sources (local storage and a remote sync service) each insert the same record. Common triggers include:

What Users See

In the UI, the same name appears two or more times, often with identical phone numbers or email addresses. Tapping either entry may open the same detail screen, but actions like “Delete” or “Merge” can behave inconsistently—sometimes removing only one copy, leaving a phantom entry. Power users notice the clutter quickly; casual users may simply feel the list is “messy.”

Reproducing the Bug

  1. Start with a clean device or emulator.
  2. Add a contact named “Alice Smith” with number +1 555‑123‑4567.
  3. Trigger a contact import (e.g., via Settings → Accounts → Google → Sync Contacts).
  4. Repeat the import while the device is offline, then reconnect to force a retry.
  5. Open the contact list and scroll to verify two Alice Smith rows.

Detecting Duplicates Automatically

A simple assertion can be added to UI tests:


// Android Espresso example
@Test
fun contactListHasNoDuplicates() {
    val names = onView(withId(R.id.contact_name))
            .perform(RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(0))
            .check(matches(isDisplayed()))
            .extract { it.text.toString() }
    val uniqueNames = names.distinct()
    assertEquals(names.size, uniqueNames.size, "Duplicate contact names found")
}

For web apps, a Playwright snippet:


test('no duplicate names', async ({ page }) => {
    await page.goto('/contacts');
    const names = await page.$$eval('.contact-name', els => els.map(e => e.textContent.trim()));
    const unique = [...new Set(names)];
    expect(names.length).toBe(unique.length);
});

Fixing the Root Cause

Preventing Recurrence

---

Common Contact List Bugs and How to Catch Them: Missing Phone Number Formatting

Cause

Applications often store phone numbers as raw strings entered by the user. When displaying, they forget to apply locale‑specific formatting (e.g., adding parentheses, spaces, or dashes). The bug surfaces when the app later tries to dial the number directly from the string, leading to failed calls because the dialer expects a canonical E.164 format.

Symptom

A contact shows “(555) 123‑4567” in the UI, but tapping the call button initiates a dial to “5551234567” (missing the country code) or, conversely, the stored value is “+15551234567” while the UI shows “15551234567” without the plus sign. Users in regions that rely on prefixes (e.g., Japan’s leading zero) see calls fail silently.

Reproduction Steps

  1. Add a contact with number +44 20 7946 0958 (UK format).
  2. Verify the UI displays it as “020 7946 0958” (national format).
  3. Tap the call button and observe the dialed string in the device’s call log (use adb logcat | grep Dialer).
  4. The log shows the number without the leading “+44”, causing the call to be routed incorrectly.

Detection via Automated Checks

Create a test that calls the formatting function and compares output against a known‑good library (e.g., libphonenumber).


@Test
void phoneNumberFormatterUsesE164ForDialing() {
    PhoneNumberUtil util = PhoneNumberUtil.getInstance();
    Phonenumber.PhoneNumber numberObj = util.parse("+44 20 7946 0958", "GB");
    String formatted = util.format(numberObj, PhoneNumberUtil.PhoneNumberFormat.E164);
    assertEquals("+442079460958", formatted);
    // Ensure the UI layer calls the same method before invoking Intent.ACTION_DIAL
}

In a web context, use Jest:


test('formatter returns E164 for dial', () => {
    const input = '+44 20 7946 0958';
    const formatted = formatForDial(input); // function under test
    expect(formatted).toBe('+442079460958');
});

Fix

Prevention

---

Common Contact List Bugs and How to Catch Them: Incorrect Sorting / Ordering

Why Sorting Fails

Contact lists are often sorted by display name, but the app may neglect to normalize case, ignore diacritics, or use the raw Unicode code point order. Additionally, when contacts are grouped by account type (e.g., Google vs. Exchange), the app might concatenate lists without re‑sorting the combined result.

User Impact

Names appear out of alphabetical order, making it hard to locate a contact quickly. For example, “Ábel” may appear after “Zara” because the app sorts by raw UTF‑8 values where ‘Á’ (U+00C1) sorts after ‘Z’ (U+005A). Users with non‑Latin scripts may see their contacts scattered throughout the list.

Reproducing the Issue

  1. Add contacts: “Anna”, “Ábel”, “Zoe”, “Mike”.
  2. Open the contact list and observe the order.
  3. Expected: Anna, Ábel, Mike, Zoe (locale‑aware collation).
  4. Actual (broken): Anna, Mike, Zoe, Ábel.

Automated Detection

Use a test that retrieves the displayed list and verifies it matches a locale‑sorted array.


@Test
fun contactListIsLocaleSorted() {
    val displayed = onView(withId(R.id.recycler_view))
            .perform(RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(0))
            .check(matches(isDisplayed()))
            .extract { it.text.toString() }
    val expected = displayed.sortedWith(Collator.getInstance(Locale.getDefault()))
    assertEquals(expected, displayed)
}

For web, with Playwright:


test('list is sorted according to locale', async ({ page }) => {
    await page.goto('/contacts');
    const items = await page.$$eval('.contact-item', els => els.map(e => e.textContent.trim()));
    const sorted = [...items].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
    expect(items).toEqual(sorted);
});

Fix

Prevention

---

Common Contact List Bugs and How to Catch Them: Contact Merge Failures

Origin

When a user edits a contact that exists in multiple accounts (e.g., a Google contact and a device‑only contact), the app should merge the fields rather than create a duplicate. Merge logic can fail when:

What the User Experiences

After editing “Bob Lee” to add a second phone number, the user sees two Bob Lee entries: one with the original number, another with the new number. Attempting to delete one may remove both, or the UI may show a “Merge” button that does nothing. The inconsistency erodes confidence in the address book.

Steps to Reproduce

  1. Create a contact “Bob Lee” with number +1 555‑000‑1111 in the Google account.
  2. Sync the device so the contact appears locally.
  3. Disable Google sync temporarily.
  4. Edit the local copy, adding a second number +1 555‑000‑2222.
  5. Re‑enable Google sync and wait for the merge.
  6. Open the contact list and verify only one Bob Lee entry exists with both numbers.

Detecting Merge Problems

An end‑to‑end test can assert the final state:


@Test
void contactMergeResultsInSingleEntryWithAllFields() {
    // Given
    Contact google = new Contact("Bob Lee", Set.of("+15550001111"));
    Contact local = new Contact("Bob Lee", Set.of("+15550002222"));
    repository.save(google);
    repository.save(local);

    // When
    repository.syncAccounts();

    // Then
    List<Contact> contacts = repository.findAllByName("Bob Lee");
    assertEquals(1, contacts.size());
    assertTrue(contacts.get(0).getNumbers().containsAll(Set.of("+15550001111", "+15550002222")));
}

Fix

Prevention

---

Common Contact List Bugs and How to Catch Them: Sync Failures with Backend

Cause

Contact sync relies on network calls, authentication tokens, and conflict‑resolution protocols. Failures arise when:

Symptom

After a period of offline use, the user returns online and notices that newly added contacts on the web portal do not appear in the app. Conversely, deletions made on the server may still linger locally, producing “ghost” entries.

Reproduction

  1. Log in to the app and let it perform an initial sync (verify 10 contacts).
  2. Using a web portal, add a new contact “Charlie Doe” and delete an existing contact “Eve”.
  3. Force the app to go offline (enable airplane mode).
  4. Bring the device back online and trigger a manual sync (pull‑to‑refresh).
  5. Open the contact list: Charlie Doe is absent, Eve is still present.

Automated Sync Verification

Use a mock server (e.g., MockWebServer) to simulate success, token expiry, and pagination.


@Test
func syncHandlesTokenRefreshAndPagination() {
    // Enqueue first page
    server.enqueue(MockResponse()
        .setResponseCode(200)
        .setBody("""{"contacts":[{ "id":"1","name":"A" }],"nextCursor":"c1"}"""))
    // Enqueue token expiry then refresh
    server.enqueue(MockResponse().setResponseCode(401))
    server.enqueue(MockResponse()
        .setResponseCode(200)
        .setBody("""{"contacts":[{"id":"2","name":"B"}],"nextCursor":null}"""))
    // Trigger sync
    viewModel.syncContacts()
    // Advance virtual time
    advanceTimeBy(5, TimeUnit.SECONDS)
    // Verify both contacts persisted
    val contacts = repository.getAll()
    assertEquals(2, contacts.size)
    assertTrue(contacts.any { it.name == "A" })
    assertTrue(contacts.any { it.name == "B" })
}

Fix

Prevention

---

Common Contact List Bugs and How to Catch Them: Permission Handling Errors

Why Permissions Break

Modern OSes require runtime permissions for accessing contacts (Android READ_CONTACTS, WRITE_CONTACTS; iOS CNContactStore). Bugs appear when:

User Experience

Tapping the “Add Contact” button yields a blank screen or a toast saying “Unable to access contacts”. The user may think the app is broken and abandon the flow.

Reproducing the Fault

  1. Deny the contact permission in device settings.
  2. Launch the app and navigate to the contact picker.
  3. Observe that the app either crashes (null pointer) or shows an empty spinner indefinitely.

Automated Permission Checks

On Android, use UiAutomator to verify the permission dialog appears and the app handles the result:


@Test
void permissionDenialShowsExplanation() {
    // Revoke permission via adb
    UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
            .executeShellCommand("pm revoke com.example.app android.permission.READ_CONTACTS");
    // Launch contact picker
    onView(withId(R.id.pick_contact)).perform(click());
    // Expect a Snackbar or Toast with a helpful message
    onView(withText(R.string.permission_contacts_explanation))
            .check(matches(isDisplayed()));
}

On iOS with XCTest:


func testContactAccessDeniedShowsAlert() {
    CNContactStore().requestAccess(for: .contacts) { granted, error in
        // Simulate denial
        XCTAssertFalse(granted)
    }
    let picker = CNContactPickerViewController()
    picker.delegate = self
    present(picker, animated: true)
    // Wait for alert
    let expectation = self.expectation(description: "alert appears")
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        XCTAssertNotNil(self.presentedViewController as? UIAlertController)
        expectation.fulfill()
    }
    waitForExpectations(timeout: 2, handler: nil)
}

Fix

Prevention

---

Common Contact List Bugs and How to Catch Them: Accessibility Label Missing

Root Cause

Accessibility services (TalkBack, VoiceOver) rely on content descriptions or ARIA labels to convey purpose. When developers set only a visual text label and forget to assign contentDescription (Android) or aria-label/aria-labelledby (web), screen readers announce the raw view type (“button”, “image”) instead of the intended action.

Effect on Users

A visually impaired user cannot tell whether tapping an avatar will open the contact profile, start a chat, or delete the entry. This leads to missed interactions, accidental deletions, and abandonment of the app.

How to Reproduce

  1. Enable TalkBack (Android) or VoiceOver (iOS).
  2. Navigate to a contact list item.
  3. Listen to the spoken output: you hear “Button” rather than “Call John Doe”.
  4. Repeat for the “Edit”, “Delete”, and “Share” actions.

Automated Accessibility Checks

Use the Accessibility Test Framework (Android) or axe-core (web) in CI.

Android Espresso + AccessibilityTest:


@Test
fun contactItemHasContentDescription() {
    onView(withId(R.id.contact_item))
            .check(matches(hasDescendant(withContentDescription(containsString("Call")))))
            .check(matches(hasDescendant(withContentDescription(containsString("Edit")))))
            .check(matches(hasDescendant(withContentDescription(containsString("Delete")))))
}

Web Playwright + axe:


import { injectAxe, checkA11y } from 'jest-axe';

test('contact list passes axe', async ({ page }) => {
    await page.goto('/contacts');
    await injectAxe(page);
    const { violations } = await page.evaluate(async () => {
        return await axe.run();
    });
    expect(violations).toHaveLength(0);
});

Fix

Prevention

---

Common Contact List Bugs and How to Catch Them: Crash on Long Press / Context Menu

Why Crashes Happen

Long‑press gestures often trigger a ContextMenu or ActionMode. Crashes occur when:

What the User Sees

The app suddenly closes, presenting a system dialog (“App keeps stopping”). The user loses any unsaved edits and may distrust the app’s stability.

Steps to Reproduce

  1. Populate the contact list with at least one entry.
  2. Disable network and trigger a background sync that clears the list temporarily (e.g., via a mock server returning an empty array).
  3. While the list is empty or in the process of updating, long‑press on a visible item (the stale view may still be present).
  4. Observe a crash in Logcat: NullPointerException: Attempt to invoke virtual method ... on a null object reference.

Detecting the Crash with Automated Tests

Use Espresso’s perform(longClick()) combined with IdlingResource to wait for background operations, then assert that no uncaught exception occurs.


@Test
fun longPressDoesNotCrashWhenListIsUpdating() {
    // IdlingResource that tracks a LiveData loading state
    val idle = object : IdlingResource {
        // ... implementation omitted for brevity
    }
    IdlingRegistry.getInstance().register(idle)

    onView(withId(R.id.recycler_view))
            .perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(0, longClick()))

    // Verify a context menu appears (no crash)
    onView(withText(R.string.menu_edit)).check(matches(isDisplayed()))

    IdlingRegistry.getInstance().unregister(idle)
}

Fix

Prevention

---

Common Contact List Bugs and How to Catch Them: Search Functionality Broken

Source of Defects

Search typically involves a query string that is matched against contact fields (name, phone, email). Bugs appear when:

User Impact

A user types “Jon” expecting to find “Jonathan” and “Jonatan”, but sees no results. They may assume the contact does not exist and create a duplicate, worsening the duplication problem.

Reproducing the Fault

  1. Add contacts: “Jonathan Smith”, “Jonatan Liu”.
  2. In the search bar, type “jon” (lowercase).
  3. Observe that the list shows zero results.
  4. Change to “JON” (uppercase) and see results appear (if case‑sensitive bug).

Automated Search Verification

Parameterized test that feeds various query forms and checks the result set.


@ParameterizedTest
@CsvSource(
    "jon, Jonathan Smith, Jonatan Liu",
    "JON, Jonathan Smith, Jonatan Liu",
    "jo n, Jonathan Smith", // space inside query should be ignored
    "jonathan, Jonathan Smith"
)
fun searchReturnsCorrectMatches(query: String, vararg expectedNames: String) {
    viewModel.setSearchQuery(query)
    // Wait for debounce
    advanceTimeBy(300, TimeUnit.MILLISECONDS)
    val displayed = viewModel.contactList.value?.map { it.name } ?: emptyList()
    expectedNames.forEach { assertTrue(displayed.contains(it)) }
}

Fix

Prevention

---

Common Contact List Bugs and How to Catch Them: Internationalization / Unicode Issues

Why Unicode Breaks

Contact names may contain characters outside the ASCII range: accented letters (é, ñ), CJK glyphs, emojis, or right‑to‑left scripts (Arabic, Hebrew). Problems arise when:

Effect on Users

A contact named “👩‍🚀 Astronaut” may appear as “👩‍🚀 Astron” (truncated after 50 bytes). An Arabic name “محمد” may be displayed left‑aligned, breaking readability. Users with non‑Latin names feel the app is “broken” for them.

Reproducing the Issue

  1. Add a contact with name “🧙‍♂️ Merlin 🧙‍♀️”.
  2. View the contact in the list and the detail screen.
  3. Observe that the emojis are missing or replaced with .
  4. For a length test, add a name exactly 150 Unicode characters long (which may be >150 bytes).
  5. Sync with the backend and verify the name is truncated.

Automated Unicode Checks

Use a test that sets a known Unicode string and asserts the retrieved

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free