join()

ES3+

Creates and returns a new string by concatenating all elements in an array.

Syntax

array.join(separator)

Parameters

separator string optional

String to separate each element (default: comma)

Return Value

string

A string with all array elements joined

Examples

JavaScript
const words = ['Hello', 'World'];
console.log(words.join(' '));
console.log(words.join('-'));
Output:
// 'Hello World' 'Hello-World'

📌 When to Use

Use join() whenever an array needs to become one string with a chosen delimiter between elements: comma-separated values for a CSV row or query parameter, "/" for URL and file paths, ", " for human-readable lists, "\n" for multi-line text, or "" to fuse characters back together after split-based string surgery. It is the second half of the split/join pair, and round-tripping through split(delim) and join(delim) is a standard technique for delimiter replacement. join() also underpins the fastest idiomatic way to build long strings: push fragments into an array, then join once at the end, instead of growing a string with += in a loop. Choose join() over template literals when the number of parts is dynamic - templates excel at fixed layouts like `${a}-${b}`, while join() handles "however many items the user selected". Two alternatives deserve mention for display text: toLocaleString() applies locale-aware element formatting before joining with commas, and Intl.ListFormat produces grammatically correct lists ("A, B, and C") in the user's language, which hand-rolled join(", ") plus "and" logic only approximates for English. Remember the elements themselves are stringified with standard ToString semantics: objects become "[object Object]" unless they define toString(), so map elements to strings deliberately first.

⚠️ Common Mistakes

Forgetting that null and undefined stringify to EMPTY strings rather than "null"/"undefined": [1, null, 3].join("-") is "1--3". The delimiters still appear, producing doubled separators that break naive CSV consumers and path builders. Filter out nullish entries first, or map them to an explicit placeholder before joining.

Calling join() with no argument and expecting concatenation without separators. The default separator is a comma, so ["a","b","c"].join() is "a,b,c" - identical to String(arr). Seamless fusion requires the explicit empty string: join(""). The mistake is common right after split("") in string-reversal and character-manipulation code.

Growing a string with += inside a large loop. Strings are immutable, so each += conceptually creates a fresh string; engines mitigate this with internal rope structures, but the push-then-join pattern remains the predictable, allocation-friendly way to assemble output from thousands of fragments, such as generated HTML rows.

Joining objects directly and shipping "[object Object],[object Object]" to users. join() stringifies each element with ToString, and plain objects have no useful default. Map to the field you mean first: users.map(u => u.name).join(", ").

Building CSV by joining raw values with commas. Any field containing a comma, quote, or newline silently corrupts the row structure - "Smith, John" becomes two columns. Real CSV needs per-field quoting and escaping (or a library); join(",") alone is only safe for values you control.

✅ Best Practices

Compose line-based output with join("\n"): log excerpts, generated code, email bodies, terminal tables. Building the lines as an array first keeps each line testable on its own, and the final join guarantees no stray trailing newline - a detail loops with += reliably get wrong.

Chain filter(Boolean) before joining when parts are conditionally present: [title, subtitle, badge].filter(Boolean).join(" - ") drops the missing pieces AND their separators in one motion, avoiding the "Title - - Badge" artifacts that plague hand-concatenated display strings.

Treat map-then-join as one composed idiom for formatted output: items.map(i => i.name).join(", ") first normalizes every element to the string you intend, then joins. Keeping formatting in the map step means the join line never needs to know about object shapes.

For user-facing lists, prefer new Intl.ListFormat(locale).format(names) over join(", ") - it inserts the conjunction and localized punctuation correctly ("A, B, and C" in English, different patterns elsewhere), which a plain delimiter can never do.

When building URLs, join the path segments with "/" but encode each segment first: segments.map(encodeURIComponent).join("/"). Joining raw user input invites broken links and injection of extra path levels via embedded slashes.

⚡ Performance Notes

join() runs in a single native pass: the engine measures the total length, allocates the result string once, and copies the pieces in - O(n) in the combined output size with no intermediate strings. That single-allocation profile is why push-then-join is the classic recommendation over += concatenation in loops, although modern V8 narrows the gap by representing concatenated strings as ropes (deferred concatenations) that flatten lazily. In practice: for a handful of fragments, use whatever reads best, including template literals; for assembling thousands of fragments - server-side HTML rendering, CSV export, code generation - the array + join() pattern gives dependable linear behavior and less GC churn. Costs scale with output size, so joining a million short strings is fast while joining strings totaling hundreds of megabytes is a memory event regardless of method; stream to a file or response in chunks at that scale instead of materializing one giant string.

🌍 Real World Example

Building a File Path

Two everyday join() jobs in one example. Path building keeps segments as an array for as long as possible - easy to validate, filter, or prepend to - and derives the final string only at the boundary where a path is actually needed; with user-supplied segments you would map each through encodeURIComponent first. The second half shows the display-list problem: humans expect "A, B and C", not "A, B, C", so the code joins all but the last element normally and attaches the final one with a conjunction. That template-literal-plus-slice dance is exactly what Intl.ListFormat automates with correct grammar across languages, but the manual version remains common where localization is not required.

const pathSegments = ['users', 'john', 'documents', 'report.pdf'];
const filePath = pathSegments.join('/');
// filePath: 'users/john/documents/report.pdf'

// Creating a comma-separated list with "and" before the last item
const authors = ['Alice', 'Bob', 'Charlie'];
const formatted = authors.length > 1
  ? `${authors.slice(0, -1).join(', ')} and ${authors.slice(-1)}`
  : authors[0];
// formatted: 'Alice, Bob and Charlie'

Related Methods