Halfway through the month a finance sheet grew a red row: one cell somewhere in the column returned #DIV/0!, and every =SUM that touched it collapsed into the same error. Wrap it in IFERROR? That silences the noise, but the running totals in row 200 still stop making sense as soon as a filter hides a chunk of rows. AGGREGATE is the function Excel added for exactly this pair of problems — a column that mixes clean numbers, error cells, and rows the reader has filtered away — and it handles both in one formula, without an IFERROR wrapper and without switching to SUBTOTAL.

Why SUM lies about your data

Two situations quietly break the sum at the bottom of a column. The first is a single error cell — a division by zero, a bad lookup, a stray #REF! — which poisons every ordinary formula that touches the range. The second is filters. A pivot-adjacent totals row shows the total for everything in the sheet, not everything visible on screen, which is almost never what the reader wants.

The usual patches each have a cost. =SUM(IFERROR(A2:A200,0)) hides the errors but also hides the fact that a lookup broke — the underlying data problem is now invisible. SUBTOTAL respects filters but ignores errors only by accident, and it silently swallows other SUBTOTAL rows nested inside its range, which is a separate feature you have to remember.

Warning. Wrapping a range in IFERROR to “clean up” a total tells the reader everything is fine while a broken formula sits upstream. Fix the source cell; do not paper over it in the total.

The point of AGGREGATE is that you say out loud what you want ignored — errors, hidden rows, both, or neither — and get one clean result. The messy cells stay visible where they belong; the total just refuses to be dragged down by them.

What AGGREGATE actually does

The function takes a math operation as a number, an “ignore what” option as a number, and the range. Two forms exist because a few of the underlying functions need an extra k argument (LARGE, SMALL, PERCENTILE). Both forms live inside one AGGREGATE call — the k does not sit outside it.

=AGGREGATE(function_num, options, ref1, [ref2], …)
=AGGREGATE(function_num, options, array, [k])

function_num is 1 through 19 and picks the operation: 1 is AVERAGE, 9 is SUM, 4 is MAX, 14 is LARGE, and so on down the standard statistical set. options is 0 through 7 and picks what to skip. Everything else is the range, exactly like SUM. Per the Microsoft AGGREGATE reference, the function is available from Excel 2010 onward and works in both Windows and Mac builds.

The two arguments people forget

Beginners write =AGGREGATE(9, A2:A200) and get a #VALUE! back. Excel is not being difficult — the second argument is mandatory. Even if you want to ignore nothing, you write 4 in the options slot. Think of it as “always name the operation, always name the exclusion, then hand it the data.”

The options themselves are a lookup by intent. Decide what you want to skip in the range, read the row, use that number. Every option also skips nested AGGREGATE and SUBTOTAL results except for options 4 through 7 — the “no nested skip” half of the table.

Option Hidden rows Errors Nested SUBTOTAL / AGGREGATE
0 Keep Keep Skip
1 Skip Keep Skip
2 Keep Skip Skip
3 Skip Skip Skip
4 Keep Keep Keep
5 Skip Keep Keep
6 Keep Skip Keep
7 Skip Skip Keep

The distinction between the top four rows and the bottom four looks fussy, and most beginner formulas will only ever use 3, 6, or 7. Reach for the 0–3 half when you are building a summary block that sits above other AGGREGATE or SUBTOTAL rows — Excel then treats those subtotal rows as invisible so your grand total does not double count.

Ignoring errors without an IFERROR wrapper

The most common reason people meet AGGREGATE is a single #DIV/0! or #N/A in the middle of a column. The old fix — wrapping every cell in IFERROR — spreads a fix across two hundred rows to solve a problem in one. AGGREGATE with option 6 collapses that into one formula.

Say column B holds a per-row conversion rate — clicks over impressions — and a couple of impression cells came in at zero. The formulas in column B return #DIV/0!, and the average at the bottom is broken. This is what the range looks like:

A B C
1 Campaign Rate Note
2 Alpha 0.032
3 Bravo #DIV/0! no impressions yet
4 Charlie 0.041
5 Delta 0.028

The average you actually want is 0.034 — Alpha, Charlie, and Delta divided by three, with Bravo left out. =AVERAGE(B2:B5) returns #DIV/0!. =AGGREGATE(1, 6, B2:B5) returns 0.034. The two arguments in front are the operation (1 for AVERAGE) and the exclusion (6 for “errors only”).

Before.

=AVERAGE(IFERROR(B2:B5,""))

Works, but hides the broken lookup from the reader.

After.

=AGGREGATE(1, 6, B2:B5)

Same answer. The #DIV/0! cell stays visible for someone to fix.

That last point matters more than the keystrokes saved. A total that quietly rejects broken cells still leaves them broken in the sheet, so the next reader can find and repair them. See the site’s Excel error troubleshooting notes for what each error type actually signals before you decide to skip it.

Playing nicely with filters

The other everyday use is running totals on filtered data. SUBTOTAL has done this job since the 1990s, and for a plain sum on a filtered list it is still fine. AGGREGATE earns its place when the range also contains error cells, because SUBTOTAL propagates errors instead of skipping them.

Options 5 and 7 are the two you actually type. Option 5 ignores hidden rows only — errors still surface, which is often what you want mid-analysis. Option 7 ignores hidden rows and errors together, for the report row a stakeholder will see.

=AGGREGATE(9, 5, D2:D5000)     // sum of visible rows, errors still surface
=AGGREGATE(9, 7, D2:D5000)     // sum of visible rows, errors quietly skipped
=SUBTOTAL(9, D2:D5000)         // sum of visible rows, but one #N/A breaks it

The number 9 in front of the range is “SUM” — the same code SUBTOTAL uses. If you have muscle memory for SUBTOTAL codes 1 through 11, they map to the same operations at the same positions in AGGREGATE’s 1 through 11.

Autofilter, not manual row hiding

“Hidden rows” here means rows the user has hidden with an Autofilter or by right-clicking and choosing Hide. It does not distinguish between the two ways rows disappear. If you filter a table and also hide a row manually, both are excluded — there is no option to skip only one class.

LARGE, SMALL, and the k argument

Six of the nineteen functions need a “which one?” number: LARGE (14), SMALL (15), PERCENTILE.INC (16), QUARTILE.INC (17), PERCENTILE.EXC (18), QUARTILE.EXC (19). That number is k. It goes inside the AGGREGATE call as the last argument, not after it — a common mistake is writing =AGGREGATE(14, 6, A2:A100), 2, which is a syntax error, instead of =AGGREGATE(14, 6, A2:A100, 2).

A concrete use: the second-highest sales figure in a column that also holds a #N/A from a broken lookup.

=AGGREGATE(14, 6, Sales[Amount], 2)

14 is LARGE, 6 ignores errors, Sales[Amount] is a structured reference to the table column, and 2 asks for the second largest. LARGE alone would have returned #N/A. Percentiles work the same way — =AGGREGATE(16, 6, Latency, 0.95) reads “give me the 95th percentile of the Latency range, ignoring any error cells.”

Tip. If you keep mistyping k position, remember: everything inside the parentheses is one argument list. The comma before 2 belongs inside the call, not after the closing paren.

Common gotchas

Three limits catch people out. Fix them once and the function behaves.

  • Horizontal ranges do not respect hidden columns. AGGREGATE was designed for vertical ranges. Hide a column and its values still count. The Microsoft reference above notes this explicitly; there is no workaround inside the function.
  • Deleted rows are not hidden rows. A row you deleted is gone from the range’s perspective, and no option is needed. Options 1, 3, 5, and 7 only affect rows that still exist but are not showing.
  • Text values are always ignored. Numeric functions like SUM and AVERAGE skip text automatically, regardless of option. Do not add an option 6 to “handle text” — it is not what option 6 is for.

One more, worth its own paragraph: AGGREGATE in a dynamic-array context spills like any other function, and if a stray error appears inside the spilled range it will not automatically get skipped by a downstream total. Skipping happens inside a single AGGREGATE call, not across the sheet. If your dynamic array is throwing spill errors of a different kind, the site’s spill-error guide walks through the usual causes.

When to reach for something else

Two adjacent cases are not AGGREGATE’s job. To sum only rows that match a condition, use SUMIFS. To strip empty entries from a text range so numeric functions run cleanly, use TEXTJOIN with the ignore-empty flag — see the site’s TEXTJOIN notes for the shape of that argument. AGGREGATE only decides what to skip inside an existing operation; it does not filter by content.

Turning it into muscle memory

Faced with a broken total, the shortest path from problem to formula is a four-step check. Run through it once and the option number falls out naturally:

  1. Is the range one vertical column? If not, AGGREGATE is the wrong tool — use SUMIFS or a helper column.
  2. Do errors exist in the range you want left out? If yes, option 6 or 7.
  3. Is the sheet filtered or are rows hidden by hand? If yes, option 5 or 7.
  4. Are other AGGREGATE or SUBTOTAL rows sitting inside the range? If yes, drop into the 0–3 half so those subtotal rows are ignored too.

Then pick the operation number — 1 AVERAGE, 9 SUM, 4 MAX, 14 LARGE — and write the formula. Once the shape is muscle memory (operation, exclusion, range), the messy sheets that used to require an IFERROR patch and a helper column collapse into a single line. Start with option 6 the next time an errant #DIV/0! breaks a monthly sum; add a 7 when a filter enters the picture; keep the broken cells where they are so someone can still fix them.

You have a sales sheet with thousands of rows and one question: what’s the biggest deal we closed for the West region last quarter? Plain MAX gives you the largest number in a column — the wrong answer here, because it ignores region and date. That’s what MAXIFS and MINIFS are for. They return the largest or smallest value from a range, but only from the rows that pass every condition you list. Same shape as SUMIFS and COUNTIFS, same rules, one answer per formula.

The syntax borrows from SUMIFS

If you already write SUMIFS and COUNTIFS, MAXIFS is the same shape with a different first argument. The value column comes first, then the criteria pairs.

=MAXIFS(max_range, criteria_range1, criteria1, [criteria_range2, criteria2], ...)
=MINIFS(min_range, criteria_range1, criteria1, [criteria_range2, criteria2], ...)

max_range (or min_range) is the column you want the biggest or smallest number from. Each criteria_range/criteria pair filters the rows before Excel picks the extreme value. You can chain up to 126 pairs, per the Microsoft MAXIFS reference, but two or three is where most real formulas land.

Both functions arrived in Excel 2019 and ship in every version since — Microsoft 365, Excel 2021, Excel 2024, and the Mac equivalents. Older builds don’t have them; the fallback lives at the end of this article.

Note. Criteria ranges do not need to sit next to the max range. They only have to match its size and shape — a rule that quietly causes most of the errors you’ll see later.

A first example: biggest and smallest sale by region

Say row 1 holds the headers Region, Salesperson, Amount, Date, and rows 2 through 200 hold individual deals. A miniature version:

A B C D
1 Region Salesperson Amount Date
2 West Priya 18400 2026-02-11
3 East Diego 9200 2026-02-14
4 West Priya 27100 2026-03-05
5 West Ken 6300 2026-03-11

To find the largest West-region deal:

=MAXIFS(C2:C200, A2:A200, "West")

C2:C200 is the amount column, A2:A200 is the region column, and "West" is the criterion. Swap MAXIFS for MINIFS to get the smallest. Add a second pair to narrow down to one rep:

=MAXIFS(C2:C200, A2:A200, "West", B2:B200, "Priya")

Both criteria have to pass on the same row for that row’s amount to enter the pool. Text matches are case-insensitive, so "west" and "West" return the same result.

Text, wildcards, and dates as criteria

The third argument accepts more than a literal string. Three patterns cover almost every real formula.

Wildcards. Use * for any run of characters and ? for a single character. To grab the biggest deal from any region starting with “West”:

=MAXIFS(C2:C200, A2:A200, "West*")

Comparison operators. Wrap the operator in quotes and glue in a reference with &. Filtering to deals above a threshold in cell K2:

=MAXIFS(C2:C200, C2:C200, ">"&K2)

Date ranges. Combine two comparison criteria over the same date column. Biggest West-region sale in Q1 2026:

=MAXIFS(C2:C200, A2:A200, "West", D2:D200, ">="&DATE(2026,1,1), D2:D200, "<"&DATE(2026,4,1))

The one syntax rule everyone forgets: the operator goes inside the quotes, the value stays outside, and & joins them.

Before.

=MAXIFS(C:C, D:D, ">K2")

Excel reads “>K2” as literal text and matches nothing.

After.

=MAXIFS(C:C, D:D, ">"&K2)

The operator is quoted, the cell reference is concatenated.

The zero-on-no-match trap

When no row matches, MAXIFS and MINIFS both return 0 — not #N/A, not blank. That is fine when zero is impossible in your data (a positive-only sales amount, for instance), and it silently poisons every dashboard where zero is a valid answer: temperature deltas, KPI variances, refund adjustments, portfolio returns.

Warning. A cell that shows 0 could mean “no rows matched” or “the biggest value really was zero.” The formula alone can’t tell you which.

Wrap the call so the two cases don’t look the same. Add a COUNTIFS gate:

=IF(COUNTIFS(A2:A200,"West",B2:B200,"Priya")=0,
     "No match",
     MAXIFS(C2:C200,A2:A200,"West",B2:B200,"Priya"))

On Microsoft 365, LET keeps the max and the count in one pass so you don’t repeat the criteria:

=LET(
   m, MAXIFS(C2:C200,A2:A200,"West"),
   c, COUNTIFS(A2:A200,"West"),
   IF(c=0,"No match",m))

The specific formula matters less than the habit. Any time MAXIFS feeds a dashboard cell, ask whether a real zero and a “nothing matched” zero should look the same. If not, guard the formula.

Fetching the row that produced the max

MAXIFS returns the value, not the invoice number, the rep, or the date behind it. For that you pair it with INDEX and XMATCH:

=INDEX(B2:B200,
       XMATCH(1,
              (A2:A200="West") * (C2:C200 = MAXIFS(C2:C200,A2:A200,"West")),
              0))

Read it inside-out: MAXIFS finds the biggest West amount; the multiplication builds a 1/0 array flagging the row where region is West and amount equals that max; XMATCH finds the first 1; INDEX returns the salesperson’s name on that row.

Tip. Ties are silent — XMATCH returns the first winning row and hides the rest. If ties matter, run FILTER(B2:B200, (A2:A200="West")*(C2:C200=MAXIFS(...))) instead to see every row that hits the max.

On Excel 2019 there’s no XMATCH. Use MATCH entered with Ctrl+Shift+Enter — the array-formula workaround does the same job with older syntax.

When ranges don’t line up — the #VALUE! fix

The most common MAXIFS bug is a range mismatch. C2:C200 and A2:A201 return #VALUE!. So does C2:C200 paired with A:A, because Excel refuses to compare 199 cells against a full column of 1,048,576.

Warning. Every criteria_range must have the same number of rows and columns as the max_range. Not the same starting row — the same length.

Three ways to keep them synchronized, in order of preference:

  1. Convert the source to an Excel Table (Ctrl+T). Deals[Amount] and Deals[Region] are guaranteed the same length and grow with new rows.
  2. If you must use raw ranges, keep every reference literal and identical: C2:C200 on the value side, A2:A200 on every criteria side.
  3. If you use full-column references, use them everywhere: C:C and A:A, never A:A paired with B2:B200.

The Table option is the one that survives new data. The other two are one refactor away from breaking.

If you’re stuck on Excel 2016 or older

MAXIFS doesn’t exist before Excel 2019, and the same is true for MINIFS. The classic fallback is MAX or MIN wrapped around an IF array formula:

{=MAX(IF((A2:A200="West")*(D2:D200>=DATE(2026,1,1)), C2:C200))}

Enter it with Ctrl+Shift+Enter; the curly braces appear automatically. IF returns the amount when both conditions are true and FALSE otherwise, and MAX ignores logical values, so only the qualifying amounts contribute.

For MIN, swap the outer function — same array-formula shape. If you find yourself writing multi-criteria lookups every week on an old build, the honest recommendation is to upgrade. MAXIFS, FILTER, and the dynamic array family are the reason to move.

Wrapping up

Start with the SUMIFS shape you already know: value column first, criteria pairs second. Be defensive about two things — the silent zero when nothing matches, and the #VALUE! when your ranges drift out of sync. Guard every dashboard cell with a COUNTIFS check the first time you drop MAXIFS into it. When you need the row behind the number, pair with INDEX and XMATCH. Everything else — wildcards, date windows, multi-column filters — is the same pattern with a different third argument.

  • ✓ Value column is the first argument, criteria pairs follow
  • ✓ Every criteria range matches the size and shape of the max range
  • ✓ Comparison operator inside quotes, cell reference concatenated with &
  • ✓ Dashboard cells wrap MAXIFS with COUNTIFS to distinguish a real zero from no match
  • ✓ Row-behind-the-number lookups use INDEX + XMATCH against the max

The fill-handle habit is so ingrained it hides its cost. A month-end refresh: filter out old rows, drag the invoice numbers again, then chase the two people who edited a copy before the drag. A quarterly report: extend the date column by hand, remember the working days shift when a holiday lands mid-week. Each pass is cheap on its own. The sheet quietly breaks the moment somebody inserts a row and forgets to re-fill. SEQUENCE replaces that pattern with a single formula that spills across the range you ask for and recalculates when the arguments change. The maintenance step disappears with it.

Why dragging the fill handle keeps costing you time

Fill-handle sequences are static. The instant the underlying count changes — a new customer, a shorter month, an added workday — you either re-drag or accept broken references downstream. Nothing warns you. The formula that consumed the sequence still returns a value, just against the wrong length.

SEQUENCE moves the count into the formula itself. Instead of “start at 1001 and drag to row 47,” you write the intent: give me 47 rows starting at 1001, stepping by 1. If the row count comes from a cell — a COUNTA against the source column, a table row count, an explicit input — the range grows or shrinks automatically the next time the sheet recalculates. Insert a customer, the ID column extends. Delete two, it contracts. Nothing on your end.

Note. SEQUENCE is a dynamic-array function. It works in Microsoft 365, Excel 2021, Excel 2024, and Excel for the web. Older builds (Excel 2019 and below) don’t support dynamic arrays and will show #NAME? for the formula.

That one trade — a single formula for a live count — replaces three habits at once: hand-typed invoice numbers, hard-coded month headers, and helper columns whose only purpose is a ROW() arithmetic trick pretending to be a counter. Every workbook has a few of each, and each one is a small liability the next time somebody inserts a row above them.

The four arguments and what happens when you skip them

The signature is =SEQUENCE(rows, [columns], [start], [step]). Only rows is required; the other three default to 1. That gives four common shapes with almost no typing, and the differences show up cleanly in a grid.

A B C
1 Formula Output shape Reads as
2 =SEQUENCE(6) 6 rows × 1 col 1..6 down
3 =SEQUENCE(1, 6) 1 row × 6 cols 1..6 across
4 =SEQUENCE(5, 4) 5 × 4 grid 1..20 row-first
5 =SEQUENCE(5, 1, 1001, 1000) 5 rows × 1 col 1001, 2001, … 5001

Two shape rules matter for anything more complex. The grid fills by row first — row 1 gets values 1 through cols, row 2 gets cols+1 through 2×cols, and so on. And step accepts negatives: =SEQUENCE(10, 1, 10, -1) counts down 10 to 1 in a single column. See Microsoft’s official SEQUENCE reference for the full argument table.

Number series patterns that ship as one formula

Once the argument shapes click, several familiar workbook habits collapse into a single cell. The examples below are the ones that pay for themselves fastest.

Invoice or ticket IDs that grow with the source table. Replace a helper column that concatenates a prefix with ROW()-1:

="INV-"&TEXT(SEQUENCE(COUNTA(Table1[Customer])), "0000")

COUNTA reads how many customers the table has right now; SEQUENCE emits that many integers; TEXT pads to four digits. Add a customer and INV-000N+1 appears without touching the ID column.

A GL code series with a large step. Chart-of-accounts blocks often step by a round number so subsidiary codes fit between them:

=SEQUENCE(6, 1, 1000, 100)

Returns 1000, 1100, 1200, 1300, 1400, 1500. Change the row count in one place to extend the block; the downstream lookups keep referencing the spill range and follow along without a re-point.

Tip. Reference a spill range with the hash suffix — =E2# — so downstream formulas resize with the SEQUENCE output instead of pointing at a fixed range. This is the difference between a one-time win and a workbook that stays clean for a year.

A padded pagination header. Reports that print in blocks of 25 often need “Page 1 of N” rows generated once and reused. =SEQUENCE(CEILING(COUNTA(Data[ID])/25, 1)) gives the row count for the page numbering column in a single spill.

Dates and workdays without a hand-typed calendar

SEQUENCE really pays off once the series is dates. Excel stores dates as numbers, which means SEQUENCE can generate them directly and other date functions accept the array as input without any wrapping.

A 30-day rolling calendar starting today:

=SEQUENCE(30, 1, TODAY(), 1)

Format the spill range as Short Date. The list re-anchors every time the workbook opens.

Monthly headers for a 12-month plan, always aligned to the current calendar year:

=EDATE(DATE(YEAR(TODAY()), 1, 1), SEQUENCE(1, 12, 0))

DATE resolves to January 1 of the current year; SEQUENCE(1, 12, 0) emits 0 through 11 across a row; EDATE adds each of those as a month offset. Format the row as mmm-yy and the header refreshes itself every January.

Working days only, skipping weekends and a holiday list you keep on another sheet:

=WORKDAY(TODAY()-1, SEQUENCE(20), Holidays[Date])

WORKDAY takes an offset array from SEQUENCE and returns the next 20 working days after today, skipping weekends and anything in the named table Holidays. Change the 20 to a cell reference and the schedule length becomes user-driven.

Warning. TODAY() is volatile — every recalculation shifts the sequence forward. If auditors need a fixed date column for the report period, paste the SEQUENCE spill as values once the calendar is set, or drive the start from a cell you edit deliberately.

When #SPILL blocks the result and how to unblock it

SEQUENCE refuses to render if anything sits in its spill range. Excel returns #SPILL! instead of overwriting existing cells, which is the correct behavior, but the error message doesn’t always point at the culprit. Three causes account for almost every real case in a working file.

  1. A stray value in the spill area. Click the formula cell, hover the small warning icon that appears, then choose “Select Obstructing Cells” — Excel navigates straight to the blocker. Clear the offending cell and the formula settles into place.
  2. Merged cells overlap the target range. Dynamic arrays cannot spill into merged cells at all. Unmerge the entire target region before the formula will spill, even if the merged block is only two cells wide.
  3. The spill lands inside an Excel Table. Structured tables reserve one value per cell; a spilling formula hands back several. Move the SEQUENCE call to a cell outside the table, then reference the spill from inside the table if you need it there.

All three feel obvious after the fact. The trap is when the blocker sits far below the formula — a stray “Total” label that used to anchor the old drag-fill range, or a hidden row you forgot about. If the same workbook keeps throwing spill errors after those checks, the deeper #SPILL troubleshooting checklist walks through the less-common causes that survive the first pass.

Migrating a real workbook from drag-fill to SEQUENCE

The upgrade is usually a two-cell change: replace the manual ID column with a SEQUENCE call, then point every dependent lookup at the spill range instead of the old fixed range. The before/after below is the shape it takes in an invoice workbook, and the same pattern applies to date columns, tag numbers, and any other counter that grew by drag.

Before.

A2: 1001
A3: =A2+1
(drag A3 down to A200)

Breaks when rows are inserted mid-column, and silently under-counts when the customer list grows past row 200.

After.

A2: =SEQUENCE(COUNTA(B2:B10000), 1, 1001)

Grows and shrinks with column B. Nothing to re-drag, nothing to forget at month end.

Two habits pay off during the migration. First, wrap SEQUENCE inside a LET when the arguments include a formula you’d otherwise repeat — LET keeps the row count named once instead of scattered across the sheet, which matters when the count formula is longer than a bare COUNTA. Second, if the sheet already uses SORT or SORTBY on the same range, chain them: =SORTBY(SEQUENCE(...), Sales[Rank]) reorders the output in one pass — the same pattern that dynamic sorting with SORT and SORTBY uses to build live leaderboards from a base range.

Where to make the swap first

Start with the one column that gets re-dragged the most — invoice IDs, rolling dates, sequential codes — and replace it with a SEQUENCE call driven off a COUNTA or a table reference. Verify the spill range doesn’t collide with anything below, then update the two or three lookups pointing at the fixed range so they consume the spill (E2#) instead of a hard-coded end row.

  • ✓ Pick the column that gets re-dragged most often
  • ✓ Rewrite it as one SEQUENCE call driven by a live row count
  • ✓ Repoint downstream lookups at the spill (E2#)
  • ✓ Skip the next month-end re-drag

The next month-end, the column that used to demand attention won’t. Every other pattern in this piece is a variation of that one swap.

A $30,000 car loan at 6.5% APR over five years costs $587.06 a month. Every part of that answer sits inside one Excel function, and it comes back as a negative number for a specific reason. PMT looks simple — rate, periods, principal — but small mismatches (an annual rate against monthly periods, a positive pv where you expected a positive payment) quietly produce numbers that are off by orders of magnitude. Once you set it up cleanly, you can plug in any loan and read the answer straight off the cell. You can also split the payment into interest and principal, and see what happens when you send an extra $100 a month.

PMT syntax and the sign convention that trips everyone up

PMT takes three required arguments and two optional ones. The signature comes straight from the Microsoft PMT reference:

=PMT(rate, nper, pv, [fv], [type])

rate is the interest rate per period. nper is the total number of periods. pv is the present value — the loan principal you receive today. fv is the balance you want left after the last payment (0 for a fully amortized loan). type is 0 when payments hit at the end of each period, 1 when they hit at the beginning.

Excel treats cash flowing into your account as positive and cash flowing out as negative. A loan puts money in your pocket now (positive pv), and the payments leave your pocket later (negative PMT). That is why =PMT(0.065/12, 60, 30000) returns -587.06. If you prefer a positive number on the payment row, wrap the formula in a unary minus:

=-PMT(0.065/12, 60, 30000)
Tip. Do not flip the sign by making pv negative — that changes the meaning of the model (you are lending, not borrowing) and cascades into wrong results if you ever pair PMT with the LET function or IPMT / PPMT.

Match your rate and periods, or the answer is wrong

The single most common PMT mistake is a rate that does not match the period. Type an annual rate against a monthly nper and the formula computes as if each month carried an entire year of interest, so the payment balloons. Divide the annual rate by however many payment periods fit in a year, and multiply the years by the same number to get nper.

Payment frequency rate nper (5-year loan)
Monthly APR/12 5*12 = 60
Biweekly APR/26 5*26 = 130
Quarterly APR/4 5*4 = 20
Annual APR 5

Pick your period first, then derive both rate and nper from it. If your loan agreement quotes an APR but interest actually compounds monthly, use APR/12 — that is the periodic rate the bank is billing. The tiny rounding differences that show up in the last row of the amortization schedule are a separate issue and rarely exceed a cent; the payment itself is exact.

A working loan calculator in one screen

The cleanest layout puts the inputs in column B, keeps every derived number one hop away, and shows the payment at the top. Drop this into a blank sheet and change any of the four inputs; the payment updates immediately.

A B
1 Loan amount 30000
2 Annual rate (APR) 6.5%
3 Term (years) 5
4 Payments per year 12
5 Periodic rate =B2/B4
6 Total periods =B3*B4
7 Payment =-PMT(B5,B6,B1)
8 Total paid =B7*B6
9 Total interest =B8-B1

Changing Payments per year in B4 flips the whole model — set it to 26 for biweekly and both the rate and the total periods adjust in one keystroke. That is what separates a real calculator from a one-off formula: every input has one home, and the payment formula reads from those cells instead of hard-coding numbers. Try B1 = 250000, B2 = 6.0%, B3 = 30, B4 = 12 to see a standard mortgage: $1,498.88 per period, $539,595 paid, $289,595 in interest.

Where the money goes: IPMT, PPMT, and an amortization table

PMT gives you a flat monthly number, but every payment splits into interest and principal — and the ratio shifts month by month. Two sibling functions handle that split. IPMT returns the interest portion of period N, PPMT returns the principal portion, and the two always sum to PMT.

=IPMT(rate, per, nper, pv)
=PPMT(rate, per, nper, pv)

The only new argument is per, the period number you want (1 for the first payment, 60 for the last on a 5-year monthly loan). Drop these into an amortization schedule where column A is the period number:

A B C D E
1 Period Payment Interest Principal Balance
2 1 =-PMT($B$5,$B$6,$B$1) =-IPMT($B$5,A2,$B$6,$B$1) =-PPMT($B$5,A2,$B$6,$B$1) =$B$1-D2
3 2 =$B$7 =-IPMT($B$5,A3,$B$6,$B$1) =-PPMT($B$5,A3,$B$6,$B$1) =E2-D3

Fill row 3 down to row 61 and the balance column drives itself to zero on period 60. Absolute references on B1, B5, and B6 are load-bearing here — without them, autofill shifts the loan inputs and every row lies. If you would rather keep the argument list in one place, wrap the three functions in LET so rate and nper are named once and reused across all three sibling formulas.

Extra principal: what one extra $100 a month rewrites

PMT assumes every payment is exactly the same. Send more than the fixed payment and the amortization schedule from the previous section no longer matches reality — the balance drops faster, so future interest shrinks. Modeling this needs one extra column and a switch from IPMT/PPMT to a running-balance calculation.

  1. Add a column F called Extra. Put your extra payment (say 100) in F2 and copy it down.
  2. Replace the interest column with =E1*$B$5 — the previous balance times the periodic rate.
  3. Replace the principal column with =B2+F2-C2 — base payment plus extra, minus that period’s interest.
  4. Change the balance column to =E1-D2. Stop paying the row after E first goes at or below zero.

On the $30,000 / 6.5% / 5-year loan, an extra $100 a month cuts the loan to 51 payments and drops total interest from about $5,224 to $4,321. The fixed IPMT / PPMT formulas cannot capture this because they assume a rigid schedule; once you break that assumption, you have to walk the balance forward yourself. The tradeoff is worth naming out loud: you lose the closed-form Excel functions and gain the ability to model real repayment behavior.

Balloon loans and the fv argument

Not every loan pays down to zero. Commercial mortgages, some auto leases, and interest-only structures leave a residual balance due at the end — the balloon. That is exactly what fv models: the balance you want left after the last payment. Sign it negative because it is money leaving your pocket on the last day.

Fully amortized.

=-PMT(0.065/12, 60, 30000)
=> 587.06

Loan pays down to zero on month 60.

$10,000 balloon at term end.

=-PMT(0.065/12, 60, 30000, -10000)
=> 445.55

Monthly drops, $10k still owed on month 60.

The $10,000 balloon lowers each monthly payment by about $142, but you still owe that lump sum on the last day — usually paid off with a refinance. The type argument works the same way in balloon and standard loans: set it to 1 only if the contract says payments are due at the start of the period, which is unusual for consumer loans and common for some commercial leases.

When PMT looks wrong: #NUM!, #VALUE!, and off-by-12 mistakes

Most PMT surprises fall into a small set. Run through this checklist before you assume the function is broken:

  • ✓ Rate matches the period (monthly rate for monthly nper, not annual)
  • pv is positive — the loan proceeds you received
  • ✓ Payment result is negative; flip with a leading minus if you want a positive display
  • nper is a positive whole number of periods, not a formula that returns a decimal
  • ✓ Rate cell is formatted as percent (6.5%), not as the number 6.5
  • ✓ For biweekly, rate = APR/26 and nper = years * 26, not APR/24

A #NUM! error usually means nper is zero or the rate arithmetic produced something Excel cannot solve — check that the cell you divided by is not blank. A #VALUE! error means one of the arguments is text; look for a stray apostrophe or a rate cell someone typed with a trailing percent sign into a text-formatted column. For a longer catalog of the same failure modes with screenshots, see Ablebits’ PMT function walkthrough.

Where to point PMT next

PMT is the entry point to a small family: PV solves for how much you can borrow given a payment budget, RATE reverses out an interest rate from a known payment, and NPER finds how many periods a payoff takes at a given payment. Once your inputs are laid out cleanly and the sign convention is under control, the rest of the family plugs in the same way. If you toggle often between two or three loan scenarios, wrap the calculator in SWITCH so a single input cell selects the active scenario — the payment formula itself never changes. For a ready-made single-formula template that follows the same input-in-one-column layout, see the Excel grade calculator.

Note. PMT is available in every modern build of Excel and Google Sheets; the syntax is identical across both.

A colleague in London types 03/04/2026 into a shared budget, meaning April 3rd. The sheet, still set to a US locale, files it as March 4th. Nobody notices for a week, and then a scheduled report ships with the wrong month totals. Google Sheets is not guessing at random — it is reading every cell through its locale — and the fix is almost never the Format menu. It is the setting that governs how the sheet interprets and displays every number and date across every tab.

Change the sheet’s locale

Locale is per-spreadsheet, not per-user and not per-tab. Open the file, then File → Settings. The Locale dropdown affects three things at once: the default date pattern, the default currency, and how new text entries are parsed as numbers. Google’s own help doc for locale and language spells out one detail worth pinning: changing the locale does not change your interface language — that lives in your Google Account, not the sheet.

  1. Open the sheet and choose File → Settings.
  2. Switch Locale to the region whose date order you want — United Kingdom for DD/MM/YYYY, United States for MM/DD/YYYY.
  3. Set Time zone in the same dialog if the sheet uses NOW() or TODAY() anywhere.
  4. Click Save settings. The sheet reloads; existing numeric dates reformat in place.

Locale is one-way for old cells

Any cell that already holds a real date value — the kind Sheets stores as a serial number — flips to the new pattern the moment you save. A cell that looks like a date but was pasted or imported as text stays exactly as typed, in the exact string it arrived as. That distinction is the source of most “the format won’t change” complaints.

What locale actually changes, and what it doesn’t

Every date in Google Sheets has two layers: the value (an integer counting days from December 30, 1899) and the display format applied on top of it. Locale rewrites the display format for every date-typed cell and rewrites the default parser for anything you type next. It does not touch text cells, and it does not touch cells whose format was manually overridden.

Note. Test whether a cell is a real date or a text impostor: put =ISNUMBER(A2) in an empty cell. TRUE means Sheets has a serial number underneath and the locale will reformat it. FALSE means the value is text and no format menu will move it.

The trickiest case is an ambiguous string like 01-02-2023. Under a US locale, Sheets reads that as January 2nd. Under a UK locale, February 1st. Same sheet, different interpretations depending on when the cell was entered. Flip the locale after the fact and the underlying serial number stays put — only the display shifts — so the number still means whatever the old locale decided it meant on the day it was typed.

The one-week import trap

Import a CSV that a European tool exported and you inherit its convention wholesale. If your sheet is set to US locale, rows where the day exceeds 12 (13/04/2026, 27/11/2025) get stored as text because they cannot be parsed as US dates. Rows where day and month are both ≤ 12 get silently misread. That is the row-count mismatch that shows up in a formula three days later.

Fix cells that stay stuck as text

Once you have identified a text-typed date column with ISNUMBER, the conversion path depends on how clean the strings are. DATEVALUE is the workhorse: it parses a string using the sheet’s current locale and returns the serial number, which you then format as a date.

=DATEVALUE(A2)

Fails with #VALUE! when the string does not match the sheet’s locale pattern. That is the signal — not a bug — to fix the locale first, then re-run. For strings with stray whitespace from a CSV, wrap in TRIM:

=DATEVALUE(TRIM(A2))

If the source uses a delimiter the locale does not accept (dot separators, ISO strings with a T in the middle), clean the string with REGEXREPLACE before parsing:

=DATEVALUE(REGEXREPLACE(A2, "\.", "/"))
Before.

03.04.2026  (text, left-aligned)

Sheets treats it as a string; sorting orders it lexicographically.

After.

=DATEVALUE(REGEXREPLACE(A2,"\.","/"))

Returns 46116; format the cell as a date and sorting works.

Paste the formula result over the original column with Paste special → Values only when you want to drop the source strings, then apply Format → Number → Date.

Custom-format one column without touching the sheet’s locale

Sometimes changing the entire sheet’s locale is wrong — the currency column is USD, but one date column needs to show ISO YYYY-MM-DD for a downstream tool. Format that column alone:

  1. Select the column.
  2. Format → Number → Custom date and time.
  3. Delete the tokens in the input at the top, then click Year, Month, Day from the picker in the order and separators you want.
  4. Save. The underlying serial numbers are unchanged; only these cells display differently.

Column-level formats survive a locale change. The sheet’s locale only overrides the default pattern — anything you explicitly set stays put.

The QUERY format shortcut

The custom-format dialog covers most needs but has to be applied to real ranges. When you need to render dates one way in an output view while keeping the source column untouched — a dashboard, a printable summary — the QUERY function’s format clause does it inline.

=QUERY(A1:C, "select A, B, C format B 'yyyy-mm-dd (ddd)'", 1)

Column B keeps its stored format in the source range. The QUERY output displays it as 2026-04-03 (Fri). Change the string in the query and the display updates instantly, no menu clicking. The format tokens are the same set the custom-format dialog uses: d, dd, ddd, dddd for day; m, mm, mmm, mmmm for month; yy, yyyy for year.

Tip. Wrap the whole query in IFERROR during dashboard builds. A single non-date value in the middle of the source column throws the whole format clause, and the sheet just shows #VALUE! with no useful message.

Timezone drift on shared sheets

The Time zone dropdown in File → Settings is easy to miss because it sits under Locale, but it decides what TODAY() and NOW() return. A sheet whose time zone is New York shows a New York date to a Sydney collaborator, even though the collaborator’s browser thinks it is already tomorrow.

Warning. Change the time zone on a sheet that already has hard-coded formulas built around midnight (payroll cutoffs, scheduled scripts) and the boundary shifts under you. Note the current value of one NOW() cell before saving, and confirm nothing is off after.

For a truly locale-neutral sheet — one that ships to teams in three regions — pin dates to ISO format explicitly with a custom date column format, and use a currency-neutral column type (numbers without the currency prefix) instead of relying on locale. That way, no future locale flip breaks anyone’s read of the numbers.

A quick diagnostic map

When a date column looks wrong, run through these in order. Most fixes land at step 2.

A B C
1 Symptom Diagnostic Fix
2 Column stuck as text =ISNUMBER(A2) → FALSE =DATEVALUE(TRIM(A2))
3 Day and month swapped Compare locale to source File → Settings → Locale
4 One column needs a different pattern Rest of sheet is fine Format → Number → Custom date
5 TODAY() off by a day Time zone mismatch File → Settings → Time zone

If the column has never been a real date, no display setting will help. That is the whole shape of the problem: locale governs display and parsing, but conversion is a formula. To keep the input clean going forward, restrict the column to valid dates with data validation so future entries cannot land as text.

Where to start

Fix the sheet’s locale first — that alone repairs anything Sheets stored as a real date. Then use ISNUMBER to find the cells that stayed text and convert them with DATEVALUE. Reach for custom date formats only when one column needs to break from the rest of the sheet, and touch the time zone only after you have checked what depends on it.

  • ✓ Set the sheet’s locale and time zone in File → Settings
  • ✓ Check =ISNUMBER(A2) before blaming the format menu
  • ✓ Convert stuck text with DATEVALUE(TRIM(...)), then paste values
  • ✓ Override a single column with Custom date; leave the rest on locale

You export a list of tags, addresses, or line items to one cell and the result reads like this: red, , blue, , , green. Empty cells in the source range become doubled commas in the output. TEXTJOIN was built to solve exactly that, but the ignore_empty switch has one blind spot that keeps burning people. This walks through the argument that fixes 90% of the mess, the one it silently leaves alone, and the FILTER-style patterns that make TEXTJOIN worth reaching for instead of chaining & operators.

What TEXTJOIN actually does

TEXTJOIN takes a range, joins every non-blank cell into a single string, and puts a delimiter of your choice between each value. It’s the modern replacement for the old ampersand-chain (A2&", "&B2&", "&C2) that everyone rewrites when a fourth column appears.

=TEXTJOIN(", ", TRUE, A2:A20)

The signature is TEXTJOIN(delimiter, ignore_empty, text1, [text2], ...). delimiter is any string — a comma, a newline, an em dash, or an empty "" to concatenate with nothing between values. ignore_empty is a Boolean. text1 onward are the ranges or literal strings to join; you can pass up to 252 of them, though a single range argument covers most real use cases.

The two functions Excel ships next to it look similar and are not:

Function Delimiter Skips blanks Best for
CONCAT None No Gluing two or three strings together
CONCATENATE None No Legacy sheets you inherit
TEXTJOIN Any string Optional A range where blanks are common

ignore_empty and the trap it doesn’t catch

Set ignore_empty to TRUE and TEXTJOIN skips truly blank cells before writing the delimiter — no doubled commas, no trailing separator. That’s the argument that cleans up the exported-list problem in one keystroke.

Before.

=TEXTJOIN(", ", FALSE, A2:A6)
red, , blue, , green

Blank cells become extra commas.

After.

=TEXTJOIN(", ", TRUE, A2:A6)
red, blue, green

Blanks are skipped cleanly.

Here’s the trap. ignore_empty checks whether the cell is empty, not whether it is visually blank. A cell that holds a single space, a stray tab, or the string "" returned from a formula is not empty to Excel — it contains a value. TEXTJOIN will happily join that value and put a delimiter around it, and your output gets a phantom entry that looks like an extra comma with nothing after it.

Cleaning the input first

The fix is to strip the whitespace before TEXTJOIN sees the cell. Wrap the range in TRIM inside an array-friendly context, or filter the range down to non-whitespace values. On dynamic-array Excel (Microsoft 365 or 2021 and up), this is a one-liner:

=TEXTJOIN(", ", TRUE, IF(TRIM(A2:A20)="", "", TRIM(A2:A20)))

The inner IF rewrites any whitespace-only cell as truly empty, and ignore_empty then does what you expected the first time. Everything else keeps its trimmed value.

Join a filtered subset without a helper column

The most useful TEXTJOIN pattern isn’t joining a whole column — it’s joining just the rows that meet a condition. Say you have a table of orders and you want a single cell listing every product a specific customer bought. The pre-2021 answer was a helper column with IF, hidden off-screen. With FILTER, the whole thing collapses:

=TEXTJOIN(", ", TRUE, FILTER(B2:B100, A2:A100=E1))

FILTER returns the product names where column A matches the customer in E1. TEXTJOIN glues them together. No helper column, no manual copy step. If the customer has no rows, FILTER returns #CALC!; wrap it in IFERROR to fall back to a friendly string.

A B C
1 Customer Product Amount
2 Acme Widget 12
3 Beta Sprocket 4
4 Acme Gasket 9
5 Acme Bolt 30

With E1 set to Acme, the formula returns Widget, Gasket, Bolt. Change the customer name and the joined list refreshes. That’s the workflow that used to need a pivot table and manual copying.

Deduping the joined list

FILTER returns every match, including repeats. Wrap it in UNIQUE to collapse duplicates before joining:

=TEXTJOIN(", ", TRUE, UNIQUE(FILTER(B2:B100, A2:A100=E1)))

If you want the reverse of this operation — pulling a joined string back apart into rows — see the notes on the TEXTSPLIT function. TEXTJOIN and TEXTSPLIT are mirror images and are usually used together during data cleanup.

Different delimiters for different rows

The delimiter argument accepts a single string, but you can pass an array of strings the same length as the number of items being joined and TEXTJOIN will rotate through them. That’s how you get a comma-and-newline effect between records without a helper column:

=TEXTJOIN({", ", CHAR(10)}, TRUE, A2:B6)

Between values inside a row: a comma. Between rows: a line break. Turn on wrap text on the destination cell or the newlines render as boxes and you’ll think the formula broke.

Tip. On Windows use CHAR(10) for the line break, not CHAR(13). Excel treats CHAR(10) as the wrap character; CHAR(13) shows as a placeholder box in most fonts.

When TEXTJOIN returns #VALUE!

TEXTJOIN has two hard ceilings and both surface as the same error. The first is the cell limit: the joined string can’t exceed 32,767 characters, because that’s the maximum length any single Excel cell can hold. Join a column of long comments and you can hit it faster than you’d guess. The second is the argument limit: you can supply at most 252 text arguments. A single range argument covers thousands of cells and does not count as thousands of arguments — 252 is only a problem if you’re passing a giant list of individual references, which almost nobody should be doing.

Warning. When TEXTJOIN returns #VALUE! and the range looks reasonable, the joined result has crossed the 32,767-character cell cap. Excel doesn’t truncate — it errors. Split the source into batches or write the result to multiple cells.

Confirming the limit before you refactor

You don’t have to guess. Wrap the range in SUMPRODUCT with LEN to see the total character count you’re about to build:

=SUMPRODUCT(LEN(A2:A20)) + (COUNTA(A2:A20)-1)*LEN(", ")

The first term sums the length of every value; the second adds the delimiter overhead. If the number is close to 32,767, refactor before you spend time chasing a phantom bug. Microsoft documents the cap in the official TEXTJOIN function reference.

Version availability and the pre-2019 fallback

TEXTJOIN shipped with Excel 2019 and is present in every Microsoft 365 build. Excel 2016 and earlier don’t have it. If you’re stuck on an older build, the closest substitute is CONCAT — but CONCAT also predates 2019, so on genuine 2013/2016 workbooks you’re back to a chained ampersand formula or a small VBA function.

  • ✓ Excel 2019 and later: TEXTJOIN available natively
  • ✓ Microsoft 365: TEXTJOIN plus FILTER, UNIQUE, SORT for the modern patterns
  • ✓ Excel 2016 and older: no TEXTJOIN — use a helper column with IF plus an ampersand chain, or a short custom VBA function
  • ✓ Excel for the web and Excel Mobile: TEXTJOIN works identically

If your team uses a mix of versions and shared workbooks, decide the target build before writing formulas that assume dynamic arrays. A TEXTJOIN + FILTER combo saved from Microsoft 365 opens in Excel 2016 as _xlfn.TEXTJOIN(...) — the formula is preserved but the result cell shows #NAME?. Related reading: the FILTER function with multiple criteria covers the same version-boundary issue for the filter side, and the TEXTBEFORE and TEXTAFTER pattern shares the same 2019/365 availability.

The one-line takeaway

TEXTJOIN with ignore_empty set to TRUE replaces the ampersand chain for almost every real join. The two things to remember are that whitespace-only cells slip past ignore_empty, and that the 32,767-character cell limit is a hard error rather than a truncation. Wrap the range in TRIM when the source is dirty, wrap it in FILTER when you only want part of it, and you’ve covered the patterns that show up in real workbooks.

Tip. The fastest first upgrade: search your workbooks for &", "& and rewrite the first one you find as TEXTJOIN(", ", TRUE, ...). You’ll spot the next candidate for it immediately.

A raw data table has 12 columns; the report needs 3 of them, in a different order, with the totals row on top. The old fix was a nest of INDEX and SEQUENCE, or a helper sheet the reader inherited from someone who has since left the team. CHOOSECOLS and CHOOSEROWS collapse that to a single spill formula: pass the source array, list the column or row numbers you want, and Excel returns exactly those slices in exactly the order you asked for. This post walks through the small syntax, the reorder pattern that turns them into report tools, the negative-index trick, the pipelines they build with FILTER and SORT, and the two errors that trip people up on real workbooks.

What CHOOSECOLS and CHOOSEROWS actually do

Both functions are dynamic-array natives introduced with the 2022 function refresh. They take a source array and one or more index numbers, and they spill a new array containing only the requested columns or rows, in the order given. CHOOSECOLS keeps every row of the source and picks columns; CHOOSEROWS keeps every column and picks rows.

=CHOOSECOLS(array, col_num1, [col_num2], ...)
=CHOOSEROWS(array, row_num1, [row_num2], ...)

array is any 2-D range or array expression — a plain range like A1:F100, a table reference like Sales[#All], or the output of another dynamic-array formula. Each index argument is a whole number: positive counts from the start, negative counts from the end, and a value that resolves to zero or exceeds the source dimension throws #VALUE!. Repeating an index is legal and duplicates the slice.

Note. Both functions require Excel for Microsoft 365 (Windows, Mac, or Web) or Excel 2024. Older perpetual builds — Excel 2021 and earlier — do not have them, and the workbook shows #NAME? when opened there. Check the official CHOOSECOLS reference on Microsoft Support before rolling a shared workbook out to a team on mixed versions.

The smallest useful example

Take a five-column source at A1:E7: Date, Rep, Region, Revenue, Margin. To spill just Rep and Revenue into G1:

=CHOOSECOLS(A1:E7, 2, 4)

Column 2 is Rep, column 4 is Revenue. The result is a two-column spill starting at G1, seven rows tall — headers included, because the source range started at row 1. That’s the whole idea. Everything below is variations on which index numbers you pass and where they come from.

Reordering columns for a report

Real reports rarely want the source order. A finance summary might want Revenue first, then Rep, then Margin — columns 4, 2, 5 from the source above. The naive way is to list them out:

=CHOOSECOLS(A1:E7, 4, 2, 5)

That works, but the index list gets awkward once you’re picking eight or ten columns, and any teammate reading it has to count columns in the source to make sense of it. The tidier pattern is an array constant — a "column recipe" you can name and reuse:

=CHOOSECOLS(A1:E7, {4,2,5})

The braces make the intent obvious: the formula picks three columns in this order. Store the recipe on a config sheet as a real range and reference it, and non-formula people on the team can edit the report layout without touching the formula.

A B C
1 Report layout Column # Source header
2 Position 1 4 Revenue
3 Position 2 2 Rep
4 Position 3 5 Margin

Feed B2:B4 straight into the formula as =CHOOSECOLS(A1:E7, TRANSPOSE(B2:B4)) — the TRANSPOSE is only there because CHOOSECOLS reads the index list horizontally when it comes from a range. Now the report layout is data, not code.

Negative indexes: pick from the end

Negative numbers count backwards from the last column or row. -1 is the last, -2 the second-to-last, and so on. The two most common uses are grabbing the totals row from the bottom of a table and pulling the last few periods off a rolling monthly sheet.

=CHOOSEROWS(Sales[#All], 1, -1)

Row 1 of the [#All] reference is the header row; row -1 is the totals row that Excel adds when you turn on Total Row under Table Design. The spill returns just those two rows — a compact header-plus-totals card for a dashboard, no extra formulas required.

Before.

=INDEX(A2:E7, ROWS(A2:E7), 0)

Reads the last row, but breaks the moment rows are added or removed unless the range is a table.

After.

=CHOOSEROWS(A2:E7, -1)

Same result, one function, and the intent ("last row") is on the page instead of hidden in a ROWS() call.

Trailing window instead of trailing row

Extend the same idea to a window. To spill the last three periods of a monthly range:

=CHOOSEROWS(Monthly[#Data], -3, -2, -1)

Order matters — the spill preserves the argument order, so -3, -2, -1 gives oldest-to-newest and -1, -2, -3 gives newest-to-oldest. If the report wants only the newest, drop the extras.

Combine with FILTER, SORT, and UNIQUE

The real payoff is chaining. CHOOSECOLS and CHOOSEROWS both accept dynamic-array output as their array argument, so the “shape then slice” pipeline is one formula deep. The pattern below builds a top-five revenue report from a full transactions table.

=CHOOSEROWS(
   SORT(
     FILTER(Sales[#Data], Sales[Region]="APAC"),
     4, -1
   ),
   SEQUENCE(5)
)

FILTER keeps only APAC rows, SORT orders them by column 4 (Revenue) descending, and CHOOSEROWS takes the first five via SEQUENCE(5). Change the region text in one cell and the whole report refreshes. If the report should also drop the internal-only columns before display, wrap the whole thing in a CHOOSECOLS:

=CHOOSECOLS(
   CHOOSEROWS( ... as above ... ),
   {1,2,4}
)

The same shape works with UNIQUE when the source has duplicate rows: run UNIQUE first, then slice. See the FILTER function walkthrough for the criteria-side patterns, the SORT and SORTBY guide for multi-column ordering, and the GROUPBY primer if the report is a summary rather than a row selection.

Common errors and what to do

Two errors account for almost every real-workbook failure with these functions: an out-of-range index and a blocked spill. Both have simple root causes once you know where to look.

Warning. #VALUE! from CHOOSECOLS means at least one index is zero or larger than the source column count. If the source is a table and a column was recently deleted, every downstream CHOOSECOLS pointing at the old position breaks silently until you edit the formula.

#VALUE! errors. The fix is almost never the formula — it’s the index list. Add a helper cell that shows =COLUMNS(source) next to the recipe range so a shrunk source is visible at a glance. If the recipe lives on another sheet, wrap each index in MIN(index, COLUMNS(source)) only as a defensive last resort; silently clamping to the last column can hide real breakage.

#SPILL! errors. The spill target has to be empty for the full result. If the report is 20 rows tall and something is sitting in row 15 of the spill area, the whole formula returns #SPILL!. Click the yellow warning triangle and Excel highlights the exact blocking cells. Move them, delete them, or spill the formula somewhere the source can grow into without hitting other content.

The insert-column trap

Index numbers are literal, not named — insert a new column in the middle of the source and every downstream CHOOSECOLS(source, 3, 5) is now pointing at different data with no error at all. This is the most dangerous class of bug because nothing turns red. Two habits keep it from biting: use a named recipe range so the recipe travels with the source, or reference the source by table column name and let a wrapper convert names to positions with MATCH.

CHOOSECOLS vs TAKE, DROP, and INDEX

Excel has three other ways to slice an array. Each is right for a different shape of problem.

Function Best for Weakness
CHOOSECOLS / CHOOSEROWS Non-adjacent columns or rows, custom order, repeats Index numbers are positional and drift when the source is edited
TAKE / DROP First-N or last-N contiguous rows or columns Can’t reorder or pick out of sequence
INDEX with SEQUENCE Rectangular slices computed from other formulas Verbose, and older readers of the workbook expect it to return a single value
FILTER Row selection by a condition, not a position Can’t select columns; pair with CHOOSECOLS when you need both

The heuristic: if the report says "the first three columns" use TAKE; if it says "columns 1, 4, and 7 in that order" use CHOOSECOLS; if it says "every row where Region is APAC" use FILTER; and if it says both "where Region is APAC" and "only columns 1, 4, 7," wrap FILTER in CHOOSECOLS. Related patterns for text extraction sit in the TEXTBEFORE and TEXTAFTER walkthrough, which fills the row-shaping side of the same toolkit.

Where to start using them

The best first place to reach for CHOOSECOLS is a report that already exists as a copy-paste of a source range — one you edit every week to match the source’s current column order. Rebuild it as one CHOOSECOLS against a named recipe range and the weekly re-alignment disappears. For CHOOSEROWS, the first win is any dashboard tile that reads "the latest row" from a growing table; the -1 index does what the reader assumed =LAST(range) would do in a language that had it.

  • ✓ Source is on Microsoft 365 or Excel 2024 — older builds show #NAME?
  • ✓ Recipe indexes live in a named range, not hard-coded in the formula
  • ✓ Spill target is empty for the full expected height and width
  • ✓ If the source table can shrink, a COLUMNS() sanity cell sits next to the recipe

Neither function does anything you couldn’t do before. They just make the intent visible on the page, which is the difference between a report a teammate can maintain and one only its author understands.

Somebody in accounting asks when the invoice is due — thirty business days after the client signed. You type =A2+30, hand back the date, and it lands on a Saturday that’s also the day after a public holiday. The client’s ops team ignores it. The invoice ages an extra week. Excel gave you a calendar-day answer to a working-day question, and calendar math and work-week math are not the same thing. WORKDAY and NETWORKDAYS are the two functions built for the difference.

Why simple date math misses the point

Excel stores every date as an integer — 2026-08-03 is just 46237 under the hood. That makes +30 and end - start tempting, and for a birthday countdown they work fine. For anything ruled by a work calendar they do not, because addition and subtraction have no concept of Saturday, Sunday, or a public holiday. Every result includes days nobody works.

The two functions solve two sides of the same question. WORKDAY takes a start date and a count, then returns the date that lands that many working days later. NETWORKDAYS takes a start and an end date and returns the count of working days in between. Both accept an optional list of holidays and both, by default, treat Saturday and Sunday as the weekend. That default matters because it’s what breaks first in every non-US or non-office context — we come back to that below.

Before.

=A2+30

Returns a date 30 calendar days out. Might be a Sunday. Might be Christmas.

After.

=WORKDAY(A2,30,Holidays)

Returns the 30th real working day. Always a weekday. Never a holiday.

WORKDAY: land on the next real working day

WORKDAY answers “what’s the date N business days from here?” — the shape a due-date column, a project deadline, or an SLA calculator needs. Its signature is short:

=WORKDAY(start_date, days, [holidays])

start_date is the anchor, days is the count to move forward, and [holidays] is an optional range or array of dates to skip. Positive days move forward; negative days move backward. That last part is easy to miss and turns out to be useful: =WORKDAY(TODAY(),-5,Holidays) is “five working days ago”, which is what any late-payment reminder should compare against.

Here is a due-date column driven off a signed-date column with a named range Holidays on another sheet:

A B C
1 Client Signed Due (Net 30 working)
2 Acme 2026-06-30 =WORKDAY(B2,30,Holidays)
3 Bolt 2026-07-15 =WORKDAY(B3,30,Holidays)

C2 resolves to 2026-08-12, not 2026-07-30. The gap is the two weekends and the July 4 observance between the signed date and the thirtieth working day. Format the result column as a short date if Excel shows the underlying serial number — WORKDAY returns a serial, and cell formatting decides how it renders.

NETWORKDAYS: count the real days between two dates

NETWORKDAYS answers the mirror question — “how many working days are in this range?” — which is what timesheets, aging reports, and turnaround-time dashboards actually want. Same three-argument shape:

=NETWORKDAYS(start_date, end_date, [holidays])

Both endpoints are counted. Give it the same date for start and end on a Wednesday and the result is 1, not 0 — a subtle thing that trips people who expect exclusive ranges. According to the Microsoft NETWORKDAYS reference, the function returns #VALUE! if any argument fails to parse as a date, which is the single most common failure and the one covered in the troubleshooting section below.

Note. NETWORKDAYS rounds datetimes down to whole days before counting. A start of 2026-08-03 14:00 and an end of 2026-08-04 09:00 both collapse to their date components, so the answer is 2 — not 1, not 0.75. Shift-hour tracking needs a different formula.

A common pattern is turnaround time per ticket. Column A holds the open date, column B the resolved date, column C uses NETWORKDAYS with a named Holidays range so a closed ticket on a Monday after a Friday holiday reads as one working day rather than four:

=NETWORKDAYS(A2, B2, Holidays)

Pair it with a simple average — =AVERAGE(C2:C500) — and you have “average working days to resolve” without a helper column filtering weekends out by hand.

Feed holidays as a Table, not a static range

The [holidays] argument accepts any range that returns dates. A fixed range like Sheet2!$A$2:$A$20 works — until the twentieth date fills up and the twenty-first holiday you add sits outside the range and silently gets counted as a working day. The fix is a one-time setup: put the holiday list in an Excel Table and reference the Table column.

  1. Put one holiday date per row on a Holidays sheet, with a header cell like “Date”.
  2. Select the range and press Ctrl+T. Confirm “My table has headers”.
  3. On the Table Design tab, rename the table to tblHolidays.
  4. Reference the column in formulas as tblHolidays[Date].

Now =WORKDAY(B2,30,tblHolidays[Date]) auto-expands the moment someone appends a new observance to the bottom of the table. No stale range, no forgotten update. This is the single change that turns WORKDAY and NETWORKDAYS from “one-off calculation” into infrastructure a team can actually rely on. If you build a lot of formulas off Tables, our guide to the essential Excel formulas every analyst leans on covers structured references in more depth.

Tip. If a public holiday falls on a Saturday, do not remove it from the list. NETWORKDAYS is smart enough to notice that day was already excluded as a weekend and will not double-subtract it. Keep the list literal — every observed holiday, weekend or not — and the counts stay correct.

When Monday–Friday isn’t your work week

The default weekend is Saturday and Sunday, and that assumption is baked into WORKDAY and NETWORKDAYS with no way to change it. Retail runs Sunday to Saturday with Wednesday off. Middle-East offices work Sunday to Thursday. A four-day week has Monday to Thursday. For any of these, the base functions lie. The .INTL variants exist for exactly this reason, with a fourth argument — the weekend code — that reshapes what “not working” means.

=NETWORKDAYS.INTL(start_date, end_date, [weekend], [holidays])
=WORKDAY.INTL(start_date, days, [weekend], [holidays])

The weekend argument takes either a number code or a seven-character binary string. The string form is easier to read once you learn it: seven characters, Monday first, 1 means non-working, 0 means working. So "0000011" is the default (Sat and Sun off), "0000110" is Fri and Sat off, and "0001000" is a Thursday-only weekend.

Weekend pattern String Number code
Saturday and Sunday off “0000011” 1
Sunday and Monday off “1000001” 2
Friday and Saturday off “0000110” 7
Sunday only off “0000001” 11
Friday only off “0000100” 16

The full number-code list is documented in the NETWORKDAYS.INTL reference. Pick whichever form you’ll re-read six months from now — most people find the string easier because it’s self-documenting. A retail schedule with Wednesday off looks like "0010000", and anyone reading the formula can count over from Monday to see what it means.

Fix the errors before they land

Three failure modes account for almost every reported problem with these functions. Ranked by how often they bite:

Warning. #VALUE! almost always means one of the date arguments is stored as text. Dates pasted from a PDF, imported from CSV, or typed with a period instead of a dash often look like dates but are strings. =ISNUMBER(A2) tells you the truth — if it returns FALSE on a cell that looks like 08/03/2026, the cell is text and the function has nothing to work with.

The fix is to wrap the offending cell in DATEVALUE()=NETWORKDAYS(DATEVALUE(A2), DATEVALUE(B2), tblHolidays[Date]) — or, better, convert the column once using Text to Columns with the Date format. One-time conversion beats formula-level patching, because the ISNUMBER check keeps working elsewhere in the workbook.

#NUM! shows up in two situations. The first is a reversed range in NETWORKDAYS — start date later than end date — which the classic function accepts and returns a negative count, but the .INTL variant rejects outright when the weekend code is also invalid. The second is a malformed weekend string: exactly seven characters, only 0 and 1, and not all sevens. "1111111" — every day is a weekend — throws #NUM! rather than returning zero, because Excel would loop forever hunting for a working day.

The third failure is quieter and worse: the formula returns a plausible number but the holiday list is wrong. Symptoms include a Christmas Day counted as a working day (holiday range doesn’t reach that far) or a duplicate holiday causing a phantom skip. This is why the Table pattern above matters — it removes the class of bug where “the range got stale” is the answer.

Pick one and start with it

If you build due-date columns, start with WORKDAY and a proper holiday Table. If you build aging or turnaround reports, start with NETWORKDAYS on the same Table. The .INTL variants come out of the box the first time someone asks about a non-standard week, and by then the muscle memory is already there. For a template that already assumes working-day math, the project timeline tracker is a reasonable place to see the pattern applied end to end.

  • ✓ Holidays live in an Excel Table, referenced as tblHolidays[Date]
  • ✓ Date columns pass =ISNUMBER(), not stored as text
  • ✓ Non-Mon–Fri weeks use the .INTL variant with an explicit weekend code
  • ✓ Result cells are formatted as short date, not general number

You have probably written the same row summary formula down a whole column: SUM in F2, drag to F2000, wait for the recalc, then hide the column because it is just clutter. BYROW replaces that pattern. Hand it a range and a small LAMBDA, and it hands back one value per row — no helper column, no fill-down, no accidental gap when someone inserts a row halfway through. BYCOL does the same across the other axis. Both take a little getting used to, and both have a set of quiet gotchas worth knowing before you rewrite half a workbook around them.

What BYROW and BYCOL actually do

Give BYROW a two-dimensional range plus a LAMBDA that reduces one row to a single value. BYROW walks top to bottom, calls the LAMBDA once per row, and stacks the results into a single column. BYCOL does the mirror image: left to right, one call per column, results stacked into a single row. The LAMBDA‘s parameter is a name of your choice — row, r, col — and inside the function it is the current slice of the array.

A concrete example: monthly sales sit in B2:E5, one product per row, four months across. To get one row total in F2 that spills down through F5:

=BYROW(B2:E5, LAMBDA(row, SUM(row)))

The output is a 4×1 dynamic array. No fill-down, no reference drift when a row gets inserted at the top. The grid below shows what the sheet looks like end to end.

A B C D E F
1 Product Jan Feb Mar Apr Total
2 Widget 120 150 90 200 =BYROW(B2:E5, LAMBDA(row, SUM(row)))
3 Gadget 80 110 140 160 490
4 Sprocket 200 170 210 180 760
5 Frame 60 70 85 95 310

Two things to know up front. The LAMBDA must return a single scalar. Return an array per row and Excel throws #CALC!, its shorthand for “this can’t nest cleanly.” Second, BYROW cannot see which row it is on. If you need the row’s position — say, to look up a matching header or offset back into the sheet — use a plain LAMBDA with MAP and pass a helper index array. BYROW gives you the row’s values but not its coordinates.

The helper-column pattern they replace

Almost every mature workbook has a “temp column” or two: a per-row calculation the author never quite deleted. Before dynamic arrays, that was the only reasonable way to summarize a row. BYROW collapses the entire column into one cell that spills. The migration is usually mechanical.

Before.

F2: =SUM(B2:E2)
   ...fill down F2:F2000

Breaks if someone deletes the fill range or inserts a row without extending the formula.

After.

F2: =BYROW(B2:E2000, LAMBDA(r, SUM(r)))

One formula, spills the full length of the input range, resizes automatically.

The single-cell version has three quiet advantages. It cannot go out of sync with the data range, because there is nothing to keep in sync. A protected sheet only needs one cell unlocked instead of two thousand. And when you eventually pair the input range with a Table or a spilled source, the output grows and shrinks along with it — no VBA, no dynamic named range, no OFFSET.

The trade-off is discoverability. A helper column is visible; a spilled BYROW result is one formula pretending to be a column. Team members editing the sheet may not realize the cells below F2 are inhabited by a spill. Add a header comment, or wrap the output range with a light fill so it reads as computed rather than editable.

Common recipes with LAMBDA

The syntax rewards keeping the LAMBDA tiny. Most useful patterns are one line long — a single call to SUM, MAX, SUMPRODUCT, or TEXTJOIN. Here are four that cover the majority of daily use.

Row total, the “hello world” example. The row argument is the horizontal slice, and SUM reduces it to one number:

=BYROW(B2:E5, LAMBDA(row, SUM(row)))

Count how many months in each row cleared a 100-unit threshold. COUNTIF is one of the few “IF” functions that plays well with an array argument, because it accepts an array where it expects a range:

=BYROW(B2:E5, LAMBDA(row, SUMPRODUCT((row>=100)*1)))

The SUMPRODUCT((row>=100)*1) pattern is the safe substitute for COUNTIF when you want zero surprises. It works because multiplying the boolean array by 1 coerces to numbers before summing.

Weighted average per row, with the weight vector hard-coded. Useful for scorecards where the four inputs are not equally important:

=BYROW(B2:E5, LAMBDA(row, SUMPRODUCT(row * {0.1, 0.2, 0.3, 0.4})))

Column maximum with BYCOL. Same shape, different axis. This spills across as a single row above the data — a compact “column high” strip:

=BYCOL(B2:E5, LAMBDA(col, MAX(col)))
Tip. Wrap a slow BYROW in LET so the range only resolves once. The LET function lets you name the range and reuse it inside the LAMBDA, which reads better and skips a second evaluation of the same expression.

The IFS trap and other functions that break inside

The first time a BYROW returns #CALC! or #VALUE! with no obvious cause, the culprit is almost always a nested function that wants a real worksheet range instead of the in-memory array chunk BYROW passes it. The *IFS family — COUNTIFS, SUMIFS, AVERAGEIFS, MAXIFS, MINIFS — is the usual offender. Their criteria arguments are declared as ranges, and the array slice does not qualify.

Two error codes, two different meanings:

Error What Excel is saying Usual cause
#CALC! Your LAMBDA returned something other than a single value. Returned an array per row, or forgot the LAMBDA entirely.
#VALUE! The LAMBDA signature or its arguments do not match what Excel expected. Wrong number of parameters, or a nested function refused the array slice.

The fix for the IFS family is to rewrite the criterion as boolean math and wrap it in SUMPRODUCT or plain SUM. Instead of COUNTIFS(row, ">=100"), use SUMPRODUCT((row>=100)*1). Instead of SUMIFS(row, row, ">0"), use SUM(IF(row>0, row, 0)). Both work with the array chunk because they never look up the range dimensions the *IFS parser insists on.

INDEX is the other common trip point. Passing an array to its row_num or column_num argument returns #VALUE! inside BYROW even when the same expression works in a normal cell. If you need indexed access, use CHOOSECOLS or INDEX with scalar indices generated by SEQUENCE outside the LAMBDA, then reference the extracted values inside.

When BYROW is overkill

Not every row summary needs a LAMBDA. Three simpler tools usually get there first, and reaching for BYROW when one of them fits makes the formula slower and harder to read.

Fill-down is still the right answer for one-off exploration in a small range. If a table has thirty rows and it will not grow, a filled SUM in a helper column is faster to type and easier for a colleague to follow. BYROW pays off when the range is dynamic, when the target sheet must stay clean, or when the calculation would repeat in many cells and you want one source of truth.

SUMPRODUCT alone handles many row-wise problems without any wrapper. If every row needs the same weighted sum against a shared constant, MMULT or a single SUMPRODUCT against the whole range often returns the same result in one pass and runs faster than BYROW, which calls the LAMBDA once per row.

GROUPBY is the right choice when the “row” is actually a category and you want a grouped aggregate — one total per region across a long transaction list, not one total per literal row of a rectangular range.

  • ✓ Range grows or shrinks over time (backing a Table, a spilled range, or a live import)
  • ✓ Calculation involves a LAMBDA or two-step logic that would be ugly to inline in every row
  • ✓ Sheet has to stay visually clean, with no helper columns hanging around
  • ✓ You want the summary and the source range to resize together, no manual maintenance

Version availability and the Google Sheets equivalent

BYROW and BYCOL both ship in Microsoft 365 and Excel 2024. Older desktop builds — Excel 2019 and Excel 2021 — do not have them, and neither do the file formats those versions produce. A workbook that opens fine in your 365 install shows the formula as _xlfn.BYROW(...) and a #NAME? error when the recipient is on 2021. Microsoft lists the versions explicitly on the official BYROW reference; treat that as the source of truth before you commit a shared file to the pattern.

Note. BYROW and BYCOL are Excel-only functions. Google Sheets does not implement them by that name, and pasting an Excel formula that uses them into a Google Sheet returns #NAME?.

Google Sheets covers the same territory with two other tools. MAP takes one or more ranges and a LAMBDA, which is the closer match. ARRAYFORMULA(SUM(...)) patterns handle simple row aggregates without any LAMBDA at all. Neither is a drop-in translation, so if a workbook needs to work in both engines, keep the summary logic explicit — a filled column with a plain SUM — rather than committing to BYROW on the Excel side and hoping the export survives.

A rewrite pass that pays off

Once BYROW clicks, the natural next step is to sweep the workbook for helper columns that exist only to hold a single formula filled down. Most of them collapse to one BYROW call. The refactor is safe: the output is still a range, still readable, still auditable — it just lives in one cell instead of two thousand. Start with the column that hurts most (usually the one the data-entry team keeps overwriting by accident), rewrite it, and see how the sheet feels lighter before touching the next one.

Two rules keep the rewrite from turning into a chore:

  • ✓ Do not migrate a calculation that is genuinely faster as a filled formula on a small fixed range.
  • ✓ Do not nest a *IFS function inside a LAMBDA without checking that it accepts an array first; rewrite as boolean math if it does not.
  • ✓ Before shipping to a colleague on Excel 2021, confirm the file will actually recalculate on their machine — otherwise fall back to a filled formula.

A tighter workbook is one cleanup pass away.

Sort a task list from the ribbon menu, add a row an hour later, and the sort is already stale. Every new entry lands at the bottom, and you re-sort. Then a teammate re-sorts, then the direction flips, then someone hides a column and the whole snapshot tears. SORT and SORTBY, added in Microsoft 365 and Excel 2021, replace that ritual with a formula. The sorted list is a spill; when the source range changes, the spill catches up on the next recalc. Same shape, no click. The only real question is which of the two you should reach for, and when.

SORT vs SORTBY: a two-line rule

The two functions are close cousins, but they answer different questions. Reach for SORT when the column you want to sort by is already part of the array you are returning — a price column inside a product table, a date column inside a log. Reach for SORTBY when the sort key lives somewhere else, or when you want to sort by a value you do not want to display.

The one-line test

Ask this: “Is the thing I am sorting by one of the columns I am about to show?” Yes means SORT. No — or “I want to hide it” — means SORTBY. The comparison below is the whole decision surface, and it holds for every sort you will write.

Question SORT SORTBY
Where is the sort key? Inside the returned array A separate array, any shape
Multi-key sort Nested SORT calls Repeated by_array / sort_order pairs
Hide the sort column? No Yes
Custom order (High/Med/Low) No Yes, via MATCH

Both spill their result as a dynamic array, and both re-run on every recalc — so once you write the formula, the sort takes care of itself.

SORT for one clean pass

SORT takes an array and returns it in order. The signature is short:

=SORT(array, [sort_index], [sort_order], [by_col])

array is the range you want sorted. sort_index is which column (or row) to sort by, counting from 1. sort_order is 1 for ascending (the default) or -1 for descending. by_col is TRUE only when your data runs left-to-right instead of top-to-bottom; leave it off almost always. Microsoft’s SORT function reference covers the same shape.

Sort by a different column

Say the sheet holds products, prices, and stock. You want the list ordered by price, most expensive first. The source lives in A1:C6:

A B C
1 Product Price Stock
2 Keyboard 79 12
3 Monitor 329 4
4 Mouse 42 30
5 Headset 120 7
6 Webcam 55 18

Drop this in E2 and let it spill:

=SORT(A2:C6, 2, -1)

Column 2 is Price. -1 means descending. Monitor pops to the top, Keyboard drops to fourth. Add a row for a new product, and the spill grows and re-sorts on the next recalc. No menu, no click.

SORTBY when the sort key lives elsewhere

SORTBY keeps the sort logic and the returned data as two separate arguments. That is the whole point: the thing you display is not tied to the thing you sort by.

=SORTBY(array, by_array1, [sort_order1], [by_array2, sort_order2], ...)

Suppose column A holds candidate names and column B holds a private interview score you do not want to publish. To return names ranked by score, high to low, without exposing the scores:

=SORTBY(A2:A11, B2:B11, -1)

The spill is one column of names. The score column drives the order and never appears in the output — impossible with plain SORT, which can only sort a column that is part of what it returns.

Multiple keys with different directions

Real sorts are almost never one-key. You want region ascending, then age descending, and you want them driven by the same live source. SORTBY stacks pairs left to right and applies them in order:

=SORTBY(A2:C11, C2:C11, 1, B2:B11, -1)

Region (column C) sorts first, ascending. Inside each region, age (column B) sorts descending. You can keep adding pairs, but three keys is usually the ceiling before the intent gets murky.

Note. Every by_array must match the row count of array exactly, and each by_array must be one column wide (or one row tall). Mismatched sizes return #VALUE!, not a helpful message.

Custom order sorting with SORTBY and MATCH

Alphabetic order is useless for values like High, Medium, Low — sorted as text they come out High, Low, Medium, which is neither the order you meant nor the order anyone reads. The fix is to sort by the position of each value inside a custom order list, and MATCH supplies that position.

=SORTBY(A2:B11, MATCH(B2:B11, {"High";"Medium";"Low"}, 0))

MATCH looks up each priority label inside the array constant and returns 1, 2, or 3. SORTBY then sorts on that numeric result, ascending, and the visible column stays in words. Curly braces with semicolons build a vertical array literal; commas would build a horizontal one, which MATCH would reject here.

The same trick for weekdays

Weekday text sorts even worse than priority. You want Mon before Tue before Wed, not Fri before Mon before Sat. Same pattern:

=SORTBY(A2:C50, MATCH(B2:B50, {"Mon";"Tue";"Wed";"Thu";"Fri";"Sat";"Sun"}, 0))

The array constant is the taxonomy; MATCH turns it into ranks; SORTBY sorts on ranks. If the labels are stored on the sheet — say a hidden lookup range in Z2:Z8 — swap the constant for the reference and the pattern still holds. This is the recipe every custom-order sort in Excel reduces to. For a longer walkthrough of the surrounding function family, see our tour of Excel functions from basic to advanced.

Keep the header row and auto-expand with tables

Two problems SORT alone will not solve show up the first time you use it for real. First, the spill contains only data — the header row is gone, because you pointed the formula at the data below the header. Second, the range is fixed: add a row past row 100 and the sort ignores it.

Point the formula at a Table

VSTACK fixes the header. Convert the source to a Table (Ctrl+T) and the range fixes itself. Combine both and the sort becomes maintenance-free.

Before.

=SORT(A2:C100, 2, -1)

No header. Row 101 is invisible. Column insertions shift the index.

After.

=VSTACK(tbl_Sales[#Headers], SORT(tbl_Sales, 2, -1))

Header preserved. Table grows, spill grows.

The Table reference does two useful things. It absorbs new rows without any range edit, and it shrugs off column insertions inside the Table because the structured reference tracks the column by name, not by position. Nested with VSTACK, the sorted output looks and behaves like a real table view. Pair this with a FILTER wrapper — see our FILTER with multiple criteria walkthrough — and you have a live view that sorts and filters at the same time.

The two errors you will actually hit

Every dynamic array function has a specific pair of failure modes, and for SORT and SORTBY they are the same two, over and over.

#SPILL! means Excel wants to write the result into cells that already hold something — a value, another formula, even a stray space. The cure is to clear the block below and to the right of the formula cell until nothing blocks it. Merged cells inside the spill area also trigger this, and they will not clear until you unmerge them.

#VALUE! from SORTBY almost always means by_array1 and array have different lengths. If array is A2:C11 (ten rows), every by_array must also be ten rows tall. Off-by-one is the common shape — B2:B10 against a ten-row array — and Excel does not point at the argument that broke.

Warning. Sort order values other than 1 or -1 also return #VALUE!. A typo like 2 for descending will not be caught until you check.

Cross-workbook links break silently

SORT and SORTBY both return #REF! when the source lives in another workbook and that workbook is closed. The formula worked yesterday; today the linked file is closed, and every spill turns red. Keep the source in the same file, or wrap the whole thing in a query that materializes the data locally. For the general dynamic-array failure playbook — including the merged-cell trap that catches everyone once — see our fix guide for the SPILL error.

One decision, one setup, done

Reach for SORT when the sort column is part of the data you are returning. Reach for SORTBY when the sort key lives elsewhere — a hidden score, a custom priority list, or several keys pulling in different directions. Wrap the result in VSTACK to keep the header row, and point it at a Table so new rows fold in without you touching the formula.

  • ✓ Sort column is inside the returned array → SORT
  • ✓ Sort key is separate or should stay hidden → SORTBY
  • ✓ Custom order (priority, weekday) → SORTBY + MATCH
  • ✓ Header row and future rows → VSTACK + Table reference

The first time you paste a wide export into Excel, you hit the same problem twice: half the columns are junk, and the ones you want are in the wrong order. The classic fix is to write an INDEX formula with an array constant, or worse, copy-paste-rearrange by hand. CHOOSECOLS and CHOOSEROWS make the same reshape a single call. They spill, they take negative indices, and they chain neatly with FILTER and SORTBY. This piece shows the syntax, then walks through the fixes those examples usually skip: combining with FILTER, working inside Excel Tables, and reading the errors when the formula breaks.

How the syntax reads

The two functions share the same shape: pass an array, then list the columns or rows you want. Nothing else.

=CHOOSECOLS(array, col_num1, [col_num2], ...)
=CHOOSEROWS(array, row_num1, [row_num2], ...)

Column and row numbers are 1-based, so =CHOOSECOLS(A2:E100, 1, 3) returns columns A and C from the range. Ask for the same column twice and you get it twice: =CHOOSECOLS(A2:E100, 1, 3, 1) spills three columns wide. Negative indices count from the right, so =CHOOSECOLS(A2:E100, -1) returns column E without your having to know how many columns there are.

CHOOSEROWS works the same way. =CHOOSEROWS(A2:E100, 1, 2, 3) returns the first three rows; =CHOOSEROWS(A2:E100, -1) returns the last row, whatever its index. Both spill, so you drop the formula into one cell and the result fans out. If you try to write into that spill range from another formula, you get #SPILL!.

Note. Zero is not a valid index. Passing 0 returns #VALUE! because Excel columns are 1-based. If your index list is being built dynamically, from SEQUENCE or SUMPRODUCT, strip zeros out before they reach CHOOSECOLS.

Availability is the other question worth answering up front. CHOOSECOLS and CHOOSEROWS live in Microsoft 365 and Excel 2024. Older builds — 2021, 2019, and the perpetual editions before 2024 — do not have them; the workbook shows #NAME? when opened on those versions. Note that once in the workbook if colleagues on older builds will read it.

When it beats INDEX with array constants

Before dynamic arrays, the standard way to pick specific columns from a range was to wrap INDEX in an array constant: =INDEX(A2:E100, SEQUENCE(99), {1,3,5}). It works, but it hides the intent — a reader has to unpack the constant to know which columns you meant, and any column insert between A and E silently shifts the numbers. CHOOSECOLS reads left to right, so the same reshape is =CHOOSECOLS(A2:E100, 1, 3, 5). If someone inserts a column at C, both formulas break — but the CHOOSECOLS version fails visibly by picking the wrong column, and you fix it by changing one number in one place.

Before.

=INDEX(A2:E100, SEQUENCE(99), {1,3,5})

The reshape sits in a constant. Reordering means editing braces; repeating a column means restructuring the whole call.

After.

=CHOOSECOLS(A2:E100, 1, 3, 5)

The reshape sits on the surface. Reordering is a comma. Repeating a column is a second reference to the same number.

There is one place INDEX still wins: legacy workbooks that need to open in Excel 2019 or the perpetual editions before 2024. Keep INDEX with the array constant for those. Everywhere else, CHOOSECOLS is shorter, self-describing, and survives audit better because the reshape is visible instead of hidden.

If you already lean on VSTACK and HSTACK to combine ranges from several sheets, CHOOSECOLS is the natural companion — stack first, then pick the columns you actually want.

A real reshape: dropping and reordering export columns

Every SaaS export ships with the same columns in the wrong order. Say your billing tool spills out Invoice ID, Client, Line Item, Amount, Currency, Status, and the report you owe your finance lead is Client, Invoice ID, Amount, Currency. That is a reorder plus a drop.

A B C D E F
1 Invoice ID Client Line Item Amount Currency Status
2 INV-1021 Acme License Q3 4200 USD Paid
3 INV-1022 Northwind Support 900 EUR Open

The single-formula fix reads the whole range and picks the four report columns in the order you want them:

=CHOOSECOLS(A2:F500, 2, 1, 4, 5)

The 2 is Client, the 1 is Invoice ID, the 4 is Amount, the 5 is Currency. Drop that into any empty cell and the result spills across four columns. When next month’s export lands in the same range, the formula recomputes. No manual work, no drift.

If you also want to relabel the output, wrap the spill in VSTACK to add a header row: =VSTACK({"Client","Invoice","Amount","Currency"}, CHOOSECOLS(A2:F500, 2, 1, 4, 5)). One formula, one recompute. If the source layout shifts, you change the numbers, not the pipeline.

Combining with FILTER and SORTBY

Most reshapes carry a filter or a sort with them. CHOOSECOLS composes cleanly with both, and the order matters: FILTER or SORTBY on the outside, CHOOSECOLS on the inside, means you narrow the rows first and then trim the columns. It is faster on wide ranges, and the formula reads in the same order you would say it out loud.

To show only paid invoices for one client, filter first, then pick the four report columns:

=CHOOSECOLS(FILTER(A2:F500, (B2:B500="Acme") * (F2:F500="Paid")), 2, 1, 4, 5)

Sort the same result by amount, largest first:

=SORTBY(
  CHOOSECOLS(FILTER(A2:F500, B2:B500="Acme"), 2, 1, 4, 5),
  FILTER(D2:D500, B2:B500="Acme"),
  -1
)

That looks dense, but every piece has one job. FILTER selects rows, CHOOSECOLS picks columns, SORTBY orders the result. When any one of them errors, Excel points at that fragment specifically.

Tip. The SORTBY sort key must match the same rows the outer expression returned. That is why the SORTBY example above re-runs FILTER on column D with the same criteria — the two spills stay row-aligned. If you sort by a column that does not line up, you scramble the report.

The one order to avoid is CHOOSECOLS wrapping FILTER’s criteria range. FILTER needs the criteria columns to line up with the source rows; if you narrow the source first, you also cut out the columns FILTER is checking against. Filter first, pick second — that order works with any dynamic-array chain, not just this one.

Using structured Table references so formulas survive column inserts

CHOOSECOLS accepts a plain range like A2:F500, but the range is fragile: insert a column at C and every hardcoded index shifts. Microsoft’s CHOOSECOLS reference lists 365 and Excel 2024 as the only builds where the function exists, and that fragility bites hardest on workbooks meant to run for years. The fix is to source from an Excel Table and reference it by name.

Turn the range into a Table with Ctrl+T, name it tblInvoices from the Table Design tab, and the formula becomes:

=CHOOSECOLS(tblInvoices, 2, 1, 4, 5)

The Table absorbs new rows automatically, so your reshape covers next month’s export without any range editing. Insert a column between Client and Line Item and the numeric indices still shift — but the failure is visible in the output, not silent.

For column names that make the intent explicit, mix in MATCH on the header row:

=CHOOSECOLS(
  tblInvoices,
  MATCH("Client",     tblInvoices[#Headers], 0),
  MATCH("Invoice ID", tblInvoices[#Headers], 0),
  MATCH("Amount",     tblInvoices[#Headers], 0),
  MATCH("Currency",   tblInvoices[#Headers], 0)
)

That verbose form buys you rename-safety and reorder-safety: shuffle the source columns however you like, and the formula still returns Client, Invoice ID, Amount, Currency because it is asking by header text, not by position. Wrap the four MATCH calls in a LAMBDA if you write this pattern often.

  • ✓ Source is an Excel Table, not a fixed A2:F500 range
  • ✓ Table has a descriptive name from Table Design, not the default Table1
  • ✓ Column headers are unique, so MATCH resolves without ambiguity
  • ✓ Header row exists and is included in [#Headers]

Troubleshooting the errors you’ll actually see

Four errors show up around CHOOSECOLS and CHOOSEROWS, and each has one fix. Learn the shapes and you stop guessing.

Error What it means Fix
#VALUE! An index is 0 or larger than the array’s column/row count. Check every literal index; if built from another formula, strip zeros and out-of-range values before they reach CHOOSECOLS.
#SPILL! The destination cannot fit the result — a value, merged cell, or another formula is in the way. Clear the target range, unmerge, or move the formula to a clean area.
#NAME? The workbook is open on a build that does not know the function (Excel 2021 or earlier). Upgrade to 365 or Excel 2024, or fall back to INDEX with an array constant for that workbook.
#CALC! A parent function was handed a spill of the wrong shape — typically a multi-column spill into a single-column input. Break the chain, name the intermediate with LET, and inspect the shape at each step.

#NAME? is the trap that catches teams first, because the workbook looks fine on the author’s laptop and only breaks when a colleague on an older build opens it. If you cannot mandate 365, add a plain-text note next to the formula so the reader knows what to look for. The same version dance bites lots of dynamic-array chains — the spill-error walkthrough covers the mirror case, where the function exists but the array collides with data below it.

When none of those fit, use Formulas → Evaluate Formula. It steps through the calculation one layer at a time. Slow, but for a broken CHOOSECOLS chain it is the fastest way to find which layer is actually wrong.

Where to start

Pick one wide export you rerun every week and rebuild the pipeline in that order:

  1. Turn the source into an Excel Table (Ctrl+T) and give it a name.
  2. Write one CHOOSECOLS to trim it to the columns your report needs, in report order.
  3. Wrap it in FILTER for the row cuts; wrap that in SORTBY only if the ordering matters.
  4. Add a VSTACK header row so the output reads as a finished report, not a raw spill.

The first time someone inserts a column upstream the formula will land on the wrong data, and the four error codes above cover almost every other case. The rest of your workflow stays the same. One formula replaces the whole copy-paste dance.

A column of raw customer strings — "Order 84213 / Priority: HIGH / TX 78701" — used to mean a three-formula sandwich of SUBSTITUTE, FIND, and MID to pull out the parts you actually cared about. Every messy row was another puzzle. Excel now has three native regex functions — REGEXTEST, REGEXEXTRACT, and REGEXREPLACE — that replace most of that gymnastics with a single call. They run on the PCRE2 engine and ship with every current Microsoft 365 build, on Windows, Mac, and the web. If a column has ever fought you on validation, extraction, or cleanup, this is the toolkit that finally fits the job.

The three functions at a glance

All three take the same first two arguments — the text to scan and the pattern to match — and then diverge on what they return. Skim the table before writing anything; it saves you from swapping functions mid-formula because the return type surprised you.

Function Signature Returns On no match
REGEXTEST text, pattern, [case] TRUE or FALSE FALSE
REGEXEXTRACT text, pattern, [mode], [case] Text or array #N/A
REGEXREPLACE text, pattern, replacement, [instance], [case] Rewritten text Original text

Two shared quirks matter from day one. The [case] argument defaults to 0, which is case sensitive. If you are used to FIND and SEARCH where SEARCH is the case-insensitive one, this flips your intuition. Pass 1 whenever a column mixes casing. Second, all three functions raise #NAME? on a build that predates the release — Microsoft’s roster of modern Excel functions keeps growing, but this trio is Microsoft-365-only per Microsoft Support’s REGEXEXTRACT reference.

REGEXTEST: the yes-or-no gate

REGEXTEST is the one you reach for when a cell needs to pass or fail a check — think validation columns, conditional formatting rules, and IF branches. It returns a Boolean, nothing else, which makes it the smallest possible replacement for a stack of ISNUMBER(SEARCH(...)) hacks.

Say column A holds job codes that must start with two capital letters and end with four digits, like MX0042 or QA9971. One formula covers the whole rule:

=REGEXTEST(A2, "^[A-Z]{2}\d{4}$")

The caret and dollar sign anchor the match to the start and end of the string, so mx0042 or a trailing space both fail. Drop that formula into a helper column, then wrap the range in a conditional-formatting rule to flag every FALSE row in red.

Case sensitivity in one line

The default trips people. If the incoming data is mx0042 and you want it to pass, either widen the character class to [A-Za-z] or add the case flag:

=REGEXTEST(A2, "^[A-Z]{2}\d{4}$", 1)
Warning. REGEXTEST defaults to case sensitive (0). That’s the opposite of SEARCH‘s default. Pass 1 as the third argument to match without regard to case.

REGEXEXTRACT: pull one match, all matches, or capture groups

REGEXEXTRACT does the work of the old MID/FIND/LEN triangle. The [mode] argument is what makes it powerful: 0 returns only the first match (the default), 1 returns every match as a spilled array, and 2 returns the capture groups from the first match as a spilled array. Each mode maps to a different real-world question.

Mode 0 — the first match

Suppose column A stores mixed shipping notes and you want the first tracking number, always six digits:

=REGEXEXTRACT(A2, "\d{6}")

\d{6} matches exactly six consecutive digits; the function returns the first run it finds and stops. Fast, simple, and enough for most extract-one-thing tasks.

Mode 2 — capture groups that spill into columns

Capture groups are where the modern regex functions stop feeling like the old text formulas. Wrap parts of the pattern in parentheses, ask for mode 2, and each group lands in its own cell across the row. A US-style phone number is the textbook example:

=REGEXEXTRACT(A2, "(\d{3})-(\d{3})-(\d{4})", 2)

Enter it once in B2. The three digit groups spill into B2, C2, and D2. That’s a full parse — area code, exchange, subscriber — in one call. Given the source data below, the grid on the right is what actually renders.

A B C D
1 Raw contact Area Exchange Line
2 Call 512-555-0134 back 512 555 0134
3 Mobile 415-555-2288 415 555 2288

Mode 1 — every match in a row

When a single cell holds several matches you want back, mode 1 returns the lot. To pull every hashtag out of a marketing note, use =REGEXEXTRACT(A2, "#\w+", 1). The result spills horizontally, one hashtag per column, and grows or shrinks with the number of matches. That behavior is close to what TEXTSPLIT does for delimiter-based cuts, but on patterns instead of fixed separators.

REGEXREPLACE: swap, mask, or reformat in one step

REGEXREPLACE searches the text for the pattern and swaps every match for a replacement string. Two features earn it a permanent slot in your kit: back-references to capture groups, and an optional [instance] argument that limits how many matches get rewritten.

To strip every non-digit from messy phone data, the pattern is a negation class and the replacement is empty:

=REGEXREPLACE(A2, "\D", "")

\D matches anything that is not a digit; replacing it with "" collapses "(512) 555-0134" down to "5125550134". Wrap it in VALUE if you need a real number for math. The same shape rebuilds addresses, cleans invoice IDs, or normalizes serial numbers.

The bigger win is swapping fragments in place. Rewriting "Smith, John" to "John Smith" is a single formula because back-references ($1, $2) point at capture groups:

Before.

=MID(A2,FIND(", ",A2)+2,99)&" "&LEFT(A2,FIND(",",A2)-1)

Three functions, two searches, brittle if the comma pattern varies.

After.

=REGEXREPLACE(A2,"(\w+),\s(\w+)","$2 $1")

One call. The pattern is the spec.

The optional fourth argument, [instance], replaces only a specific occurrence — pass 1 to change just the first match and leave the rest alone. That is the escape hatch for logs and long strings where a blanket replacement would over-reach.

Errors and gotchas you will actually hit

Each function fails differently, and that is a feature — it means the same “no match” condition surfaces with different visible symptoms depending on what you asked for. Knowing which is which stops you from wrapping every formula in IFERROR out of reflex.

  • REGEXTEST returns FALSE on no match. Never errors on a valid pattern.
  • REGEXEXTRACT returns #N/A on no match — wrap in IFERROR only if a blank is safer for downstream formulas.
  • REGEXREPLACE returns the original text unchanged on no match. No error, no blank. Silent.
  • ✓ Any invalid pattern raises #VALUE!, which is your cue that the regex itself is broken, not the data.
  • ✓ Excel uses the PCRE2 engine, so features like lookbehind and named groups work, but a pattern copied from a JavaScript tutorial may need small tweaks.

One more surprise: the pattern must be a text argument, not a range. If you want to store the regex in a cell for easy editing, reference that cell directly — REGEXTEST(A2, $F$1) is fine. Just do not paste the pattern between double quotes inside another double-quoted string.

A regex cheat sheet for spreadsheet work

You do not need to memorize regex to use it well. Ten tokens cover roughly ninety percent of business text — validate the ID, pull the number, mask the email — and everything else you can look up when the need appears. Pin this grid to a scratch sheet.

Token Matches Example
\d Any digit 0–9 \d{4} → four digits
\w Letter, digit, or underscore \w+ → one or more word chars
\s Any whitespace a\sb → “a b”
[abc] Any listed character [xyz] → x, y, or z
[^abc] Anything not listed [^0-9] → any non-digit
. Any single character a.c → “abc”, “a1c”
^ $ Start / end of text ^INV → starts with INV
? * + 0–1, 0+, 1+ repeats a+ → one or more a’s
{n,m} Between n and m repeats \d{2,4} → 2–4 digits
( ) Capture group (\d+) → group of digits

Combine those pieces and most of the everyday jobs collapse to one formula: \d{5}(-\d{4})? for US ZIPs, ^\S+@\S+\.\S+$ for a quick email sanity check, or \b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b for card-shaped numbers you may want to redact. Compared to old techniques like the ones in the TEXTBEFORE and TEXTAFTER walkthrough, regex is one layer up: a general parser instead of a fixed delimiter.

Where to start on Monday

Open the workbook that has been fighting you the most — the one with dirty phone numbers, half-typed IDs, or wildly inconsistent addresses. Pick one column. Write a REGEXTEST rule that flags every row that is off-spec, then a REGEXREPLACE that fixes the fixable rows and a REGEXEXTRACT that pulls the useful piece out of what remains.

Tip. Build the pattern once in a spare cell as a text string, then reference that cell from every regex formula in the sheet — a single edit updates every rule at once.

Three formulas, one column, one afternoon. From there, the pattern language pays back every time you meet a new pile of text — which, on any real workbook, is roughly weekly.

Scroll to Top