A Table Valued Function cannot be run by itself, it must be contained in another query. In that respect, and in many others, it is basically a parameterized view, or a saved query.
You cannot insert, update or delete using a function. While you can actually modify through the function (similar to updatable views), the function itself cannot contain any DML, or any other side-affecting commands. It is also more difficult or impossible to specify various query hints.
Stored procedures on the other hand contain the full script of what needs to be done for a particular task. They can contain almost any command you can do from an ad-hoc batch. They therefore represent a complete Unit of Work.
They are not as composable as functions though. It is very difficult to pass data from one to the other, and they are best used a single entry-point from a client app.
Due to the afore-mentioned composable and updatable abilities of functions, it might actually be worthwhile to use both together.
For example, you could have a function that returns Products that were not sold:
CREATE OR ALTER FUNCTION Product.NotSoldProducts (@From datetime, @To datetime)
RETURNS TABLE
AS RETURN
SELECT p.*
FROM Product.Product p
WHERE NOT EXISTS (SELECT 1
FROM Sales.Sale s
WHERE s.ProductId = p.Id
);
You could use this in a stored procedure to get the data, but careful use of subqueries (ie no joins) also allows this to be updatable.
CREATE OR ALTER PROC Promotion.PromoteNotSoldProducts
@From datetime,
@To datetime,
@UntilDate datetime,
@Percent int
AS
SET NOCOUNT, XACT_ABORT ON;
BEGIN TRAN;
UPDATE sp
SET PromotionPercent = @Percent,
PromotedUntil = @UntilDate
FROM Product.NotSoldProducts (@From, @To) sp;
INSERT Product.AuditLog (AuditType, Message)
SELECT p.Id, 'Promotion', CONCAT('Promoted for ', @Percent, '% until ', @UntilDate)
FROM Product.NotSoldProducts (@From, @To) sp;
COMMIT;
Note that the modifications, as well as the SET options and the transaction could not be done inside the function.