Use lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row to find the last used row in a specific worksheet column. Qualify both Cells and Rows.Count with the worksheet, start from the bottom of the column, and move up to the first non-empty cell so the code works from any active sheet.
Sub FindLastRowInColumnA()
Dim ws As Worksheet
Dim lastRow As Long
Set ws = ThisWorkbook.Worksheets("Data")
lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
MsgBox "Last row in column A: " & lastRow
End Sub
Key Takeaways
ws.Cells(ws.Rows.Count, "A").End(xlUp).Rowis the standard pattern when one key column defines the data extent- Always qualify
Cells,Rows,Range, andColumnswith the worksheet you intend to inspect - Empty columns, formatting-only cells, and data spread across multiple columns need different handling
Find the Last Row in One Column
Most macros should start with a worksheet variable, then use that variable everywhere the range is referenced. That keeps the last-row calculation tied to the intended sheet instead of whatever sheet happens to be active when the macro runs.
Here is the pattern I use when column A is required for every record:
Dim ws As Worksheet
Dim lastRow As Long
Set ws = ThisWorkbook.Worksheets("Data")
lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
The code starts at the bottom of column A on ws, then works upward exactly like pressing Ctrl + Up Arrow from the bottom of the sheet. If A1048576 is empty, it stops at the first non-empty cell above it and returns that row number.
The worksheet qualification matters. Cells(Rows.Count, "A") works only if the active sheet is the sheet you intended. In a macro that copies between sheets, imports files, or loops through worksheets, unqualified references are a common source of wrong-row bugs.
Use the Last Row in a Dynamic Range
Once lastRow is known, use it to build a range that grows and shrinks with the data. This is the practical reason most VBA macros find the last row in the first place.
Sub FormatDataRows()
Dim ws As Worksheet
Dim lastRow As Long
Set ws = ThisWorkbook.Worksheets("Data")
lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
If lastRow < 2 Then Exit Sub
ws.Range("A2:D" & lastRow).Font.Bold = False
End Sub
That If lastRow < 2 Then Exit Sub guard is useful when row 1 is a header and there may be no data rows. It prevents the macro from treating the header as a record.
You can use the same value in a loop:
Sub MarkHighValues()
Dim ws As Worksheet
Dim lastRow As Long
Dim rowNum As Long
Set ws = ThisWorkbook.Worksheets("Data")
lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
For rowNum = 2 To lastRow
If ws.Cells(rowNum, "B").Value > 100 Then
ws.Cells(rowNum, "C").Value = "High"
End If
Next rowNum
End Sub
This article stays focused on the last-row calculation. If you need the loop structure itself, see the separate guide to Excel VBA For loops. If your next step is moving the range to another sheet, see Excel VBA copy range.
Handle an Empty Column
The .End(xlUp) pattern has one important edge case: a completely empty column returns row 1. That does not mean row 1 contains data. It means Excel started at the bottom, found no non-empty cell above, and landed at the top of the sheet.
When column A may be empty, test the returned cell before using the result as a real data row:
Sub FindLastRowOrZero()
Dim ws As Worksheet
Dim lastRow As Long
Set ws = ThisWorkbook.Worksheets("Data")
lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
If lastRow = 1 And Len(ws.Cells(1, "A").Value) = 0 Then
lastRow = 0
End If
MsgBox "Data rows in column A end at row: " & lastRow
End Sub
I use 0 when I want the rest of the macro to clearly understand that no data row exists. If row 1 is a header and row 2 onward may be empty, I usually keep the header separate from the data check:
Dim lastRow As Long
lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
If lastRow < 2 Then
MsgBox "No data rows found."
Exit Sub
End If
Choose the Right Column
The best last-row column is the column that must be filled for every valid record. In a sales export, that might be the transaction ID. In a customer list, it might be the customer name or account number. Avoid using a notes column, optional amount column, or formula column that may contain blanks.
Blank rows in the middle do not break .End(xlUp) as long as there is data below them in the same column. The method searches from the bottom up, not from the top down. A blank key column at the bottom, however, will cause the macro to stop at the last non-empty key cell, even if another column has data lower down.
If the dataset can end in different columns, use Find across the worksheet or scan the columns you care about.
Find the Last Used Row Across a Worksheet
When I need the last used row anywhere on a worksheet, I use Range.Find with SearchOrder:=xlByRows and SearchDirection:=xlPrevious. Unlike checking only column A, this searches the sheet for the last cell with content by row.
Sub FindLastUsedRowOnSheet()
Dim ws As Worksheet
Dim lastCell As Range
Set ws = ThisWorkbook.Worksheets("Data")
Set lastCell = ws.Cells.Find(What:="*", _
After:=ws.Cells(1, 1), _
LookIn:=xlFormulas, _
LookAt:=xlPart, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious, _
MatchCase:=False)
If lastCell Is Nothing Then
MsgBox "No used cells found."
Else
MsgBox "Last used row: " & lastCell.Row
End If
End Sub
The If lastCell Is Nothing check is not optional on an empty worksheet. Without it, .Row raises an error because Find did not return a range.
LookIn:=xlFormulas includes cells containing formulas, even when a formula currently displays an empty string. If you only care about displayed values, use LookIn:=xlValues instead.
Find the Last Row in Specific Columns
Sometimes I want to ignore helper columns, notes, or old formulas off to the right. In that case, I check only the columns that define the dataset and keep the maximum row number.
Sub FindLastRowInSelectedColumns()
Dim ws As Worksheet
Dim lastRow As Long
Dim col As Variant
Dim candidateRow As Long
Set ws = ThisWorkbook.Worksheets("Data")
For Each col In Array("A", "B", "D")
candidateRow = ws.Cells(ws.Rows.Count, col).End(xlUp).Row
If candidateRow > 1 Or Len(ws.Cells(1, col).Value) > 0 Then
If candidateRow > lastRow Then lastRow = candidateRow
End If
Next col
MsgBox "Last row in selected columns: " & lastRow
End Sub
This is more deliberate than using every column on the sheet. It also avoids a common problem with UsedRange: a formatted cell or a previously used cell far below the actual data can make the used range look larger than the current dataset.
Methods to Avoid or Use Carefully
UsedRange
UsedRange can be useful for inspecting a worksheet, but I do not use it as my first choice for business data boundaries. It can include cells that were previously edited or formatted, even after the visible content is gone.
lastRow = ws.UsedRange.Row + ws.UsedRange.Rows.Count - 1
That returns the bottom row of Excel’s used range, not necessarily the last row of the dataset you intend to process.
SpecialCells(xlCellTypeLastCell)
SpecialCells(xlCellTypeLastCell) is fast, but it reports Excel’s last used cell, which is affected by the same used-range behavior. It may point to a formatted or formerly used area rather than the last current record.
lastRow = ws.Cells.SpecialCells(xlCellTypeLastCell).Row
Use this when you intentionally need Excel’s internal last-cell marker. Do not use it as a drop-in replacement for the last populated row in a required data column.
xlDown
I avoid Range("A1").End(xlDown).Row for last-row detection. It works only when the first cell is populated and the data below it is contiguous. If there is a blank cell inside the column, xlDown stops early. If the starting cell is blank, it can jump to an unexpected row.
Starting from the bottom and moving up is safer for ordinary data tables.
Reusable Last-Row Function
If several macros need the same check, I make it a small function that accepts the worksheet and column. That keeps the calling code readable and prevents repeated unqualified references.
Function LastRowInColumn(ByVal ws As Worksheet, ByVal columnLetter As String) As Long
Dim lastRow As Long
lastRow = ws.Cells(ws.Rows.Count, columnLetter).End(xlUp).Row
If lastRow = 1 And Len(ws.Cells(1, columnLetter).Value) = 0 Then
LastRowInColumn = 0
Else
LastRowInColumn = lastRow
End If
End Function
Example use:
Sub UseLastRowFunction()
Dim ws As Worksheet
Dim lastRow As Long
Set ws = ThisWorkbook.Worksheets("Data")
lastRow = LastRowInColumn(ws, "A")
If lastRow = 0 Then Exit Sub
ws.Range("A1:D" & lastRow).Copy _
Destination:=ThisWorkbook.Worksheets("Report").Range("A1")
End Sub
Last Row Checklist
- Use
.End(xlUp)when one column reliably identifies every data row - Use
Findwhen the last row may appear in any column on the sheet - Use a selected-column scan when only certain columns should define the dataset
- Treat a returned row of 1 carefully if the column can be empty
- Avoid unqualified
Cells,Rows,Range, andColumnsreferences in production macros - Do not rely on
UsedRangeorSpecialCells(xlCellTypeLastCell)unless you specifically want Excel’s used-range boundary
Getting the boundary right is small code, but it protects every later step in the macro: loops, copy ranges, formulas, sorts, and imports all depend on knowing where the data actually ends.
Frequently Asked Questions
Finding the last row in Excel using VBA is usually a range-boundary problem: choose the column or worksheet area that defines the data, then handle empty inputs deliberately.
What is the most efficient method to determine the last non-empty row in a specific Excel column using VBA?
For a specific required column, I use the worksheet-qualified .End(xlUp) pattern:
Dim ws As Worksheet
Dim lastRow As Long
Set ws = ThisWorkbook.Worksheets("Data")
lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
This is fast and clear when column A is populated for every valid record. If column A might be empty, check whether lastRow = 1 and A1 is blank before treating row 1 as data.
How can I automate copying a range of data up to the last populated row in Excel with VBA?
To copy data to the last row, I first determine the last row, then use that in my copy range. Here’s a simple snippet:
Dim sourceWs As Worksheet
Dim targetWs As Worksheet
Dim lastRow As Long
Set sourceWs = ThisWorkbook.Worksheets("Data")
Set targetWs = ThisWorkbook.Worksheets("Report")
lastRow = sourceWs.Cells(sourceWs.Rows.Count, "A").End(xlUp).Row
sourceWs.Range("A1:C" & lastRow).Copy Destination:=targetWs.Range("A1")
This copies columns A to C through the last populated row in column A.
What formula can I use in VBA to find and select the last row of data across multiple columns?
I usually use Find when I need the last used row across the worksheet:
Dim lastCell As Range
Dim lastRow As Long
Set lastCell = ws.Cells.Find(What:="*", _
After:=ws.Cells(1, 1), _
LookIn:=xlFormulas, _
LookAt:=xlPart, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious, _
MatchCase:=False)
If Not lastCell Is Nothing Then lastRow = lastCell.Row
This checks the sheet by rows and avoids assuming the final record is in one particular column.
Could you describe a technique for locating the final row in a dataset that includes blank rows using Excel VBA?
Blank rows in the middle are fine if the key column has data after them. Start from the bottom of the key column and move up:
lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
If data may appear below the key column in other columns, use the Find method across the worksheet instead.
What is the process for identifying the last used row in a worksheet using the xlUp method in VBA?
The xlUp method is straightforward. I typically use it like this:
lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
This finds the last used row in column A on ws. For multiple columns, repeat it for the columns that define your dataset and take the maximum.
How can I programmatically find the last completely blank row in an Excel spreadsheet using VBA?
If you mean the first row after your data table, add 1 to the last used row in the key column:
lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
blankRow = lastRow + 1
If you need a completely blank row across several columns, check that range with Application.CountA(ws.Range("A" & blankRow & ":D" & blankRow)) = 0 before writing to it.

